code-meter 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_meter/__init__.py +3 -0
- code_meter/analytics/__init__.py +7 -0
- code_meter/analytics/costs.py +43 -0
- code_meter/analytics/reports.py +357 -0
- code_meter/analytics/tokens.py +51 -0
- code_meter/cli.py +334 -0
- code_meter/config.py +116 -0
- code_meter/models/__init__.py +13 -0
- code_meter/models/project.py +85 -0
- code_meter/models/usage.py +61 -0
- code_meter/pricing/__init__.py +14 -0
- code_meter/pricing/anthropic.py +78 -0
- code_meter/pricing/engine.py +97 -0
- code_meter/pricing/google.py +60 -0
- code_meter/pricing/openai.py +69 -0
- code_meter/providers/__init__.py +15 -0
- code_meter/providers/antigravity.py +358 -0
- code_meter/providers/base.py +42 -0
- code_meter/providers/claude_code.py +378 -0
- code_meter/providers/codex.py +356 -0
- code_meter/storage/__init__.py +6 -0
- code_meter/storage/database.py +147 -0
- code_meter/storage/repository.py +482 -0
- code_meter/ui/__init__.py +19 -0
- code_meter/ui/dashboard.py +111 -0
- code_meter/ui/tables.py +195 -0
- code_meter/watcher.py +56 -0
- code_meter-0.1.0.dist-info/METADATA +207 -0
- code_meter-0.1.0.dist-info/RECORD +33 -0
- code_meter-0.1.0.dist-info/WHEEL +5 -0
- code_meter-0.1.0.dist-info/entry_points.txt +6 -0
- code_meter-0.1.0.dist-info/licenses/LICENSE +21 -0
- code_meter-0.1.0.dist-info/top_level.txt +1 -0
code_meter/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Analytics engine package."""
|
|
2
|
+
|
|
3
|
+
from code_meter.analytics.tokens import TokenAnalytics
|
|
4
|
+
from code_meter.analytics.costs import CostAnalytics
|
|
5
|
+
from code_meter.analytics.reports import ReportGenerator
|
|
6
|
+
|
|
7
|
+
__all__ = ["TokenAnalytics", "CostAnalytics", "ReportGenerator"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Cost analytics computations."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
from code_meter.models.usage import UsageRecord
|
|
5
|
+
from code_meter.pricing.engine import PricingEngine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CostAnalytics:
|
|
9
|
+
"""Calculates cumulative and per-record estimated costs."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def compute_record_costs(
|
|
13
|
+
records: List[UsageRecord],
|
|
14
|
+
pricing_engine: PricingEngine,
|
|
15
|
+
) -> Tuple[List[Optional[float]], Optional[float], int]:
|
|
16
|
+
"""
|
|
17
|
+
Compute cost for each record.
|
|
18
|
+
Returns (costs_list, total_cost_usd, unpriced_count).
|
|
19
|
+
"""
|
|
20
|
+
costs: List[Optional[float]] = []
|
|
21
|
+
total_cost: float = 0.0
|
|
22
|
+
has_any_priced = False
|
|
23
|
+
unpriced_count = 0
|
|
24
|
+
|
|
25
|
+
for r in records:
|
|
26
|
+
c = pricing_engine.calculate_cost(
|
|
27
|
+
provider=r.provider,
|
|
28
|
+
model=r.model,
|
|
29
|
+
input_tokens=r.input_tokens,
|
|
30
|
+
output_tokens=r.output_tokens,
|
|
31
|
+
cache_read_tokens=r.cache_read_tokens,
|
|
32
|
+
cache_write_tokens=r.cache_write_tokens,
|
|
33
|
+
timestamp=r.timestamp,
|
|
34
|
+
)
|
|
35
|
+
costs.append(c)
|
|
36
|
+
if c is not None:
|
|
37
|
+
total_cost += c
|
|
38
|
+
has_any_priced = True
|
|
39
|
+
else:
|
|
40
|
+
unpriced_count += 1
|
|
41
|
+
|
|
42
|
+
final_total = round(total_cost, 4) if has_any_priced else None
|
|
43
|
+
return costs, final_total, unpriced_count
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Report generator for aggregating records by model, project, session, day, and exporting."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from code_meter.analytics.costs import CostAnalytics
|
|
11
|
+
from code_meter.analytics.tokens import TokenAnalytics
|
|
12
|
+
from code_meter.models.project import (
|
|
13
|
+
AggregateSummary,
|
|
14
|
+
DailyStats,
|
|
15
|
+
ModelStats,
|
|
16
|
+
ProjectStats,
|
|
17
|
+
SessionStats,
|
|
18
|
+
)
|
|
19
|
+
from code_meter.models.usage import UsageRecord
|
|
20
|
+
from code_meter.pricing.engine import PricingEngine
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ReportGenerator:
|
|
24
|
+
"""Generates structured summaries and export formats from raw UsageRecords."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, pricing_engine: PricingEngine):
|
|
27
|
+
self.pricing_engine = pricing_engine
|
|
28
|
+
|
|
29
|
+
def generate_summary(
|
|
30
|
+
self,
|
|
31
|
+
records: List[UsageRecord],
|
|
32
|
+
filter_date_str: Optional[str] = None,
|
|
33
|
+
) -> AggregateSummary:
|
|
34
|
+
"""Build complete AggregateSummary from a list of UsageRecords."""
|
|
35
|
+
if not records:
|
|
36
|
+
return AggregateSummary()
|
|
37
|
+
|
|
38
|
+
inp, out, read, write, total_tokens = TokenAnalytics.calculate_totals(records)
|
|
39
|
+
record_costs, total_cost, unpriced_count = CostAnalytics.compute_record_costs(
|
|
40
|
+
records, self.pricing_engine
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
tokens_per_hr, tokens_per_day, cost_per_hr, cost_per_day = TokenAnalytics.calculate_burn_rate(
|
|
44
|
+
records, total_tokens, total_cost
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
by_model = self.aggregate_by_model(records, record_costs)
|
|
48
|
+
by_project = self.aggregate_by_project(records, record_costs)
|
|
49
|
+
|
|
50
|
+
today_date = filter_date_str or datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
51
|
+
today_records_with_costs = [
|
|
52
|
+
(r, c) for r, c in zip(records, record_costs)
|
|
53
|
+
if r.timestamp.strftime("%Y-%m-%d") == today_date
|
|
54
|
+
]
|
|
55
|
+
today_summary = None
|
|
56
|
+
if today_records_with_costs:
|
|
57
|
+
today_records = [rc[0] for rc in today_records_with_costs]
|
|
58
|
+
t_inp, t_out, t_read, t_write, t_total = TokenAnalytics.calculate_totals(today_records)
|
|
59
|
+
t_costs = [rc[1] for rc in today_records_with_costs if rc[1] is not None]
|
|
60
|
+
t_cost_val = round(sum(t_costs), 4) if t_costs else None
|
|
61
|
+
|
|
62
|
+
today_summary = DailyStats(
|
|
63
|
+
date_str=today_date,
|
|
64
|
+
request_count=len(today_records),
|
|
65
|
+
input_tokens=t_inp,
|
|
66
|
+
output_tokens=t_out,
|
|
67
|
+
cache_read_tokens=t_read,
|
|
68
|
+
cache_write_tokens=t_write,
|
|
69
|
+
total_tokens=t_total,
|
|
70
|
+
estimated_cost_usd=t_cost_val,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return AggregateSummary(
|
|
74
|
+
total_requests=len(records),
|
|
75
|
+
input_tokens=inp,
|
|
76
|
+
output_tokens=out,
|
|
77
|
+
cache_read_tokens=read,
|
|
78
|
+
cache_write_tokens=write,
|
|
79
|
+
total_tokens=total_tokens,
|
|
80
|
+
estimated_cost_usd=total_cost,
|
|
81
|
+
unpriced_requests=unpriced_count,
|
|
82
|
+
tokens_per_hour=tokens_per_hr,
|
|
83
|
+
tokens_per_day=tokens_per_day,
|
|
84
|
+
cost_per_hour=cost_per_hr,
|
|
85
|
+
cost_per_day=cost_per_day,
|
|
86
|
+
by_model=by_model,
|
|
87
|
+
by_project=by_project,
|
|
88
|
+
today_summary=today_summary,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def aggregate_by_model(
|
|
92
|
+
self, records: List[UsageRecord], record_costs: List[Optional[float]]
|
|
93
|
+
) -> List[ModelStats]:
|
|
94
|
+
"""Aggregate stats grouped by AI model."""
|
|
95
|
+
groups: Dict[str, Dict[str, Any]] = defaultdict(
|
|
96
|
+
lambda: {
|
|
97
|
+
"request_count": 0,
|
|
98
|
+
"input_tokens": 0,
|
|
99
|
+
"output_tokens": 0,
|
|
100
|
+
"cache_read_tokens": 0,
|
|
101
|
+
"cache_write_tokens": 0,
|
|
102
|
+
"cost": 0.0,
|
|
103
|
+
"has_priced": False,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
for r, cost in zip(records, record_costs):
|
|
108
|
+
g = groups[r.model]
|
|
109
|
+
g["request_count"] += 1
|
|
110
|
+
g["input_tokens"] += r.input_tokens
|
|
111
|
+
g["output_tokens"] += r.output_tokens
|
|
112
|
+
g["cache_read_tokens"] += r.cache_read_tokens
|
|
113
|
+
g["cache_write_tokens"] += r.cache_write_tokens
|
|
114
|
+
if cost is not None:
|
|
115
|
+
g["cost"] += cost
|
|
116
|
+
g["has_priced"] = True
|
|
117
|
+
|
|
118
|
+
result: List[ModelStats] = []
|
|
119
|
+
for model, g in groups.items():
|
|
120
|
+
tot = g["input_tokens"] + g["output_tokens"] + g["cache_read_tokens"] + g["cache_write_tokens"]
|
|
121
|
+
c_val = round(g["cost"], 4) if g["has_priced"] else None
|
|
122
|
+
result.append(
|
|
123
|
+
ModelStats(
|
|
124
|
+
model=model,
|
|
125
|
+
request_count=g["request_count"],
|
|
126
|
+
input_tokens=g["input_tokens"],
|
|
127
|
+
output_tokens=g["output_tokens"],
|
|
128
|
+
cache_read_tokens=g["cache_read_tokens"],
|
|
129
|
+
cache_write_tokens=g["cache_write_tokens"],
|
|
130
|
+
total_tokens=tot,
|
|
131
|
+
estimated_cost_usd=c_val,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
result.sort(key=lambda x: x.total_tokens, reverse=True)
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def aggregate_by_project(
|
|
139
|
+
self, records: List[UsageRecord], record_costs: List[Optional[float]]
|
|
140
|
+
) -> List[ProjectStats]:
|
|
141
|
+
"""Aggregate stats grouped by project/directory."""
|
|
142
|
+
groups: Dict[str, Dict[str, Any]] = defaultdict(
|
|
143
|
+
lambda: {
|
|
144
|
+
"project_path": None,
|
|
145
|
+
"request_count": 0,
|
|
146
|
+
"input_tokens": 0,
|
|
147
|
+
"output_tokens": 0,
|
|
148
|
+
"cache_read_tokens": 0,
|
|
149
|
+
"cache_write_tokens": 0,
|
|
150
|
+
"cost": 0.0,
|
|
151
|
+
"has_priced": False,
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
for r, cost in zip(records, record_costs):
|
|
156
|
+
p_name = r.project_id or "Default/Unknown"
|
|
157
|
+
g = groups[p_name]
|
|
158
|
+
if not g["project_path"] and r.project_path:
|
|
159
|
+
g["project_path"] = r.project_path
|
|
160
|
+
g["request_count"] += 1
|
|
161
|
+
g["input_tokens"] += r.input_tokens
|
|
162
|
+
g["output_tokens"] += r.output_tokens
|
|
163
|
+
g["cache_read_tokens"] += r.cache_read_tokens
|
|
164
|
+
g["cache_write_tokens"] += r.cache_write_tokens
|
|
165
|
+
if cost is not None:
|
|
166
|
+
g["cost"] += cost
|
|
167
|
+
g["has_priced"] = True
|
|
168
|
+
|
|
169
|
+
result: List[ProjectStats] = []
|
|
170
|
+
for p_name, g in groups.items():
|
|
171
|
+
tot = g["input_tokens"] + g["output_tokens"] + g["cache_read_tokens"] + g["cache_write_tokens"]
|
|
172
|
+
c_val = round(g["cost"], 4) if g["has_priced"] else None
|
|
173
|
+
result.append(
|
|
174
|
+
ProjectStats(
|
|
175
|
+
project_name=p_name,
|
|
176
|
+
project_path=g["project_path"],
|
|
177
|
+
request_count=g["request_count"],
|
|
178
|
+
input_tokens=g["input_tokens"],
|
|
179
|
+
output_tokens=g["output_tokens"],
|
|
180
|
+
cache_read_tokens=g["cache_read_tokens"],
|
|
181
|
+
cache_write_tokens=g["cache_write_tokens"],
|
|
182
|
+
total_tokens=tot,
|
|
183
|
+
estimated_cost_usd=c_val,
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result.sort(key=lambda x: x.total_tokens, reverse=True)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def aggregate_by_session(
|
|
191
|
+
self, records: List[UsageRecord], record_costs: List[Optional[float]]
|
|
192
|
+
) -> List[SessionStats]:
|
|
193
|
+
"""Aggregate stats grouped by session ID."""
|
|
194
|
+
groups: Dict[str, Dict[str, Any]] = defaultdict(
|
|
195
|
+
lambda: {
|
|
196
|
+
"project_name": "Unknown",
|
|
197
|
+
"project_path": None,
|
|
198
|
+
"models": defaultdict(int),
|
|
199
|
+
"request_count": 0,
|
|
200
|
+
"input_tokens": 0,
|
|
201
|
+
"output_tokens": 0,
|
|
202
|
+
"cache_read_tokens": 0,
|
|
203
|
+
"cache_write_tokens": 0,
|
|
204
|
+
"cost": 0.0,
|
|
205
|
+
"has_priced": False,
|
|
206
|
+
"first_seen": None,
|
|
207
|
+
"last_seen": None,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
for r, cost in zip(records, record_costs):
|
|
212
|
+
s_id = r.session_id or "unknown_session"
|
|
213
|
+
g = groups[s_id]
|
|
214
|
+
|
|
215
|
+
if r.project_id:
|
|
216
|
+
g["project_name"] = r.project_id
|
|
217
|
+
if r.project_path:
|
|
218
|
+
g["project_path"] = r.project_path
|
|
219
|
+
|
|
220
|
+
g["models"][r.model] += 1
|
|
221
|
+
g["request_count"] += 1
|
|
222
|
+
g["input_tokens"] += r.input_tokens
|
|
223
|
+
g["output_tokens"] += r.output_tokens
|
|
224
|
+
g["cache_read_tokens"] += r.cache_read_tokens
|
|
225
|
+
g["cache_write_tokens"] += r.cache_write_tokens
|
|
226
|
+
|
|
227
|
+
if cost is not None:
|
|
228
|
+
g["cost"] += cost
|
|
229
|
+
g["has_priced"] = True
|
|
230
|
+
|
|
231
|
+
ts_str = r.timestamp.isoformat()
|
|
232
|
+
if not g["first_seen"] or ts_str < g["first_seen"]:
|
|
233
|
+
g["first_seen"] = ts_str
|
|
234
|
+
if not g["last_seen"] or ts_str > g["last_seen"]:
|
|
235
|
+
g["last_seen"] = ts_str
|
|
236
|
+
|
|
237
|
+
result: List[SessionStats] = []
|
|
238
|
+
for s_id, g in groups.items():
|
|
239
|
+
primary_model = max(g["models"].items(), key=lambda x: x[1])[0] if g["models"] else "Unknown"
|
|
240
|
+
tot = g["input_tokens"] + g["output_tokens"] + g["cache_read_tokens"] + g["cache_write_tokens"]
|
|
241
|
+
c_val = round(g["cost"], 4) if g["has_priced"] else None
|
|
242
|
+
result.append(
|
|
243
|
+
SessionStats(
|
|
244
|
+
session_id=s_id,
|
|
245
|
+
project_name=g["project_name"],
|
|
246
|
+
project_path=g["project_path"],
|
|
247
|
+
primary_model=primary_model,
|
|
248
|
+
request_count=g["request_count"],
|
|
249
|
+
input_tokens=g["input_tokens"],
|
|
250
|
+
output_tokens=g["output_tokens"],
|
|
251
|
+
cache_read_tokens=g["cache_read_tokens"],
|
|
252
|
+
cache_write_tokens=g["cache_write_tokens"],
|
|
253
|
+
total_tokens=tot,
|
|
254
|
+
estimated_cost_usd=c_val,
|
|
255
|
+
first_seen=g["first_seen"],
|
|
256
|
+
last_seen=g["last_seen"],
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
result.sort(key=lambda x: x.last_seen or "", reverse=True)
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
def aggregate_by_day(
|
|
264
|
+
self, records: List[UsageRecord], record_costs: List[Optional[float]]
|
|
265
|
+
) -> List[DailyStats]:
|
|
266
|
+
"""Aggregate stats grouped by day (YYYY-MM-DD)."""
|
|
267
|
+
groups: Dict[str, Dict[str, Any]] = defaultdict(
|
|
268
|
+
lambda: {
|
|
269
|
+
"request_count": 0,
|
|
270
|
+
"input_tokens": 0,
|
|
271
|
+
"output_tokens": 0,
|
|
272
|
+
"cache_read_tokens": 0,
|
|
273
|
+
"cache_write_tokens": 0,
|
|
274
|
+
"cost": 0.0,
|
|
275
|
+
"has_priced": False,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
for r, cost in zip(records, record_costs):
|
|
280
|
+
d_str = r.timestamp.strftime("%Y-%m-%d")
|
|
281
|
+
g = groups[d_str]
|
|
282
|
+
g["request_count"] += 1
|
|
283
|
+
g["input_tokens"] += r.input_tokens
|
|
284
|
+
g["output_tokens"] += r.output_tokens
|
|
285
|
+
g["cache_read_tokens"] += r.cache_read_tokens
|
|
286
|
+
g["cache_write_tokens"] += r.cache_write_tokens
|
|
287
|
+
if cost is not None:
|
|
288
|
+
g["cost"] += cost
|
|
289
|
+
g["has_priced"] = True
|
|
290
|
+
|
|
291
|
+
result: List[DailyStats] = []
|
|
292
|
+
for d_str, g in groups.items():
|
|
293
|
+
tot = g["input_tokens"] + g["output_tokens"] + g["cache_read_tokens"] + g["cache_write_tokens"]
|
|
294
|
+
c_val = round(g["cost"], 4) if g["has_priced"] else None
|
|
295
|
+
result.append(
|
|
296
|
+
DailyStats(
|
|
297
|
+
date_str=d_str,
|
|
298
|
+
request_count=g["request_count"],
|
|
299
|
+
input_tokens=g["input_tokens"],
|
|
300
|
+
output_tokens=g["output_tokens"],
|
|
301
|
+
cache_read_tokens=g["cache_read_tokens"],
|
|
302
|
+
cache_write_tokens=g["cache_write_tokens"],
|
|
303
|
+
total_tokens=tot,
|
|
304
|
+
estimated_cost_usd=c_val,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
result.sort(key=lambda x: x.date_str, reverse=True)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
def export_records(self, records: List[UsageRecord], fmt: str) -> str:
|
|
312
|
+
"""Export UsageRecords to CSV or JSON format string."""
|
|
313
|
+
record_costs, _, _ = CostAnalytics.compute_record_costs(records, self.pricing_engine)
|
|
314
|
+
|
|
315
|
+
if fmt.lower() == "json":
|
|
316
|
+
export_list = []
|
|
317
|
+
for r, cost in zip(records, record_costs):
|
|
318
|
+
item = r.model_dump()
|
|
319
|
+
item["timestamp"] = r.timestamp.isoformat()
|
|
320
|
+
item["estimated_cost_usd"] = cost
|
|
321
|
+
export_list.append(item)
|
|
322
|
+
return json.dumps(export_list, indent=2)
|
|
323
|
+
|
|
324
|
+
output = StringIO()
|
|
325
|
+
writer = csv.writer(output)
|
|
326
|
+
writer.writerow([
|
|
327
|
+
"timestamp",
|
|
328
|
+
"provider",
|
|
329
|
+
"request_id",
|
|
330
|
+
"session_id",
|
|
331
|
+
"project",
|
|
332
|
+
"project_path",
|
|
333
|
+
"model",
|
|
334
|
+
"input_tokens",
|
|
335
|
+
"output_tokens",
|
|
336
|
+
"cache_read_tokens",
|
|
337
|
+
"cache_write_tokens",
|
|
338
|
+
"estimated_cost_usd",
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
for r, cost in zip(records, record_costs):
|
|
342
|
+
writer.writerow([
|
|
343
|
+
r.timestamp.isoformat(),
|
|
344
|
+
r.provider,
|
|
345
|
+
r.request_id or "",
|
|
346
|
+
r.session_id or "",
|
|
347
|
+
r.project_id or "",
|
|
348
|
+
r.project_path or "",
|
|
349
|
+
r.model,
|
|
350
|
+
r.input_tokens,
|
|
351
|
+
r.output_tokens,
|
|
352
|
+
r.cache_read_tokens,
|
|
353
|
+
r.cache_write_tokens,
|
|
354
|
+
f"{cost:.6f}" if cost is not None else "",
|
|
355
|
+
])
|
|
356
|
+
|
|
357
|
+
return output.getvalue()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Token analytics calculations (totals, burn rates, averages)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Tuple
|
|
4
|
+
from code_meter.models.usage import UsageRecord
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TokenAnalytics:
|
|
8
|
+
"""Utility methods for calculating token totals and burn rates."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def calculate_totals(records: List[UsageRecord]) -> Tuple[int, int, int, int, int]:
|
|
12
|
+
"""Return (input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, total_tokens)."""
|
|
13
|
+
inp = sum(r.input_tokens for r in records)
|
|
14
|
+
out = sum(r.output_tokens for r in records)
|
|
15
|
+
read = sum(r.cache_read_tokens for r in records)
|
|
16
|
+
write = sum(r.cache_write_tokens for r in records)
|
|
17
|
+
total = inp + out + read + write
|
|
18
|
+
return inp, out, read, write, total
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def calculate_burn_rate(
|
|
22
|
+
records: List[UsageRecord],
|
|
23
|
+
total_tokens: int,
|
|
24
|
+
total_cost: Optional[float],
|
|
25
|
+
) -> Tuple[Optional[float], Optional[float], Optional[float], Optional[float]]:
|
|
26
|
+
"""
|
|
27
|
+
Calculate burn rates:
|
|
28
|
+
Returns (tokens_per_hour, tokens_per_day, cost_per_hour, cost_per_day).
|
|
29
|
+
Returns None for values if there are fewer than 2 records or duration is 0.
|
|
30
|
+
"""
|
|
31
|
+
if len(records) < 2:
|
|
32
|
+
return None, None, None, None
|
|
33
|
+
|
|
34
|
+
sorted_records = sorted(records, key=lambda r: r.timestamp)
|
|
35
|
+
first_ts = sorted_records[0].timestamp
|
|
36
|
+
last_ts = sorted_records[-1].timestamp
|
|
37
|
+
|
|
38
|
+
duration_seconds = (last_ts - first_ts).total_seconds()
|
|
39
|
+
if duration_seconds <= 60:
|
|
40
|
+
return None, None, None, None
|
|
41
|
+
|
|
42
|
+
duration_hours = duration_seconds / 3600.0
|
|
43
|
+
duration_days = duration_seconds / 86400.0
|
|
44
|
+
|
|
45
|
+
tokens_per_hour = round(total_tokens / duration_hours, 2)
|
|
46
|
+
tokens_per_day = round(total_tokens / duration_days, 2)
|
|
47
|
+
|
|
48
|
+
cost_per_hour = round(total_cost / duration_hours, 4) if total_cost is not None else None
|
|
49
|
+
cost_per_day = round(total_cost / duration_days, 4) if total_cost is not None else None
|
|
50
|
+
|
|
51
|
+
return tokens_per_hour, tokens_per_day, cost_per_hour, cost_per_day
|