tokenator 0.1.13__py3-none-any.whl → 0.1.15__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.
- tokenator/__init__.py +2 -7
- tokenator/anthropic/client_anthropic.py +51 -15
- tokenator/base_wrapper.py +54 -8
- tokenator/migrations/versions/f028b8155fed_adding_detailed_input_and_output_token_.py +64 -0
- tokenator/models.py +33 -12
- tokenator/openai/client_openai.py +97 -5
- tokenator/schemas.py +21 -19
- tokenator/state.py +12 -0
- tokenator/usage.py +466 -232
- tokenator/utils.py +14 -1
- {tokenator-0.1.13.dist-info → tokenator-0.1.15.dist-info}/METADATA +5 -4
- tokenator-0.1.15.dist-info/RECORD +21 -0
- tokenator-0.1.13.dist-info/RECORD +0 -19
- {tokenator-0.1.13.dist-info → tokenator-0.1.15.dist-info}/LICENSE +0 -0
- {tokenator-0.1.13.dist-info → tokenator-0.1.15.dist-info}/WHEEL +0 -0
tokenator/usage.py
CHANGED
@@ -3,9 +3,16 @@
|
|
3
3
|
from datetime import datetime, timedelta
|
4
4
|
from typing import Dict, Optional, Union
|
5
5
|
|
6
|
-
|
7
6
|
from .schemas import get_session, TokenUsage
|
8
|
-
from .models import
|
7
|
+
from .models import (
|
8
|
+
CompletionTokenDetails,
|
9
|
+
PromptTokenDetails,
|
10
|
+
TokenRate,
|
11
|
+
TokenUsageReport,
|
12
|
+
ModelUsage,
|
13
|
+
ProviderUsage,
|
14
|
+
)
|
15
|
+
from . import state
|
9
16
|
|
10
17
|
import requests
|
11
18
|
import logging
|
@@ -13,257 +20,484 @@ import logging
|
|
13
20
|
logger = logging.getLogger(__name__)
|
14
21
|
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
class TokenUsageService:
|
24
|
+
def __init__(self):
|
25
|
+
if not state.is_tokenator_enabled:
|
26
|
+
logger.info("Tokenator is disabled. Database access is unavailable.")
|
27
|
+
|
28
|
+
self.MODEL_COSTS = self._get_model_costs()
|
29
|
+
|
30
|
+
def _get_model_costs(self) -> Dict[str, TokenRate]:
|
31
|
+
if not state.is_tokenator_enabled:
|
32
|
+
return {}
|
33
|
+
url = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
34
|
+
response = requests.get(url)
|
35
|
+
data = response.json()
|
36
|
+
|
37
|
+
model_costs = {}
|
38
|
+
for model, info in data.items():
|
39
|
+
if (
|
40
|
+
"input_cost_per_token" not in info
|
41
|
+
or "output_cost_per_token" not in info
|
42
|
+
):
|
43
|
+
continue
|
44
|
+
|
45
|
+
rate = TokenRate(
|
46
|
+
prompt=info["input_cost_per_token"],
|
47
|
+
completion=info["output_cost_per_token"],
|
48
|
+
prompt_audio=info.get("input_cost_per_audio_token"),
|
49
|
+
completion_audio=info.get("output_cost_per_audio_token"),
|
50
|
+
prompt_cached_input=info.get("cache_read_input_token_cost") or 0,
|
51
|
+
prompt_cached_creation=info.get("cache_read_creation_token_cost") or 0,
|
52
|
+
)
|
53
|
+
model_costs[model] = rate
|
54
|
+
|
55
|
+
return model_costs
|
56
|
+
|
57
|
+
def _calculate_cost(
|
58
|
+
self, usages: list[TokenUsage], provider: Optional[str] = None
|
59
|
+
) -> TokenUsageReport:
|
60
|
+
if not state.is_tokenator_enabled:
|
61
|
+
logger.warning("Tokenator is disabled. Skipping cost calculation.")
|
62
|
+
return TokenUsageReport()
|
63
|
+
|
64
|
+
if not self.MODEL_COSTS:
|
65
|
+
logger.warning("No model costs available.")
|
66
|
+
return TokenUsageReport()
|
67
|
+
|
68
|
+
# Default GPT4O pricing updated with provided values
|
69
|
+
GPT4O_PRICING = TokenRate(
|
70
|
+
prompt=0.0000025,
|
71
|
+
completion=0.000010,
|
72
|
+
prompt_audio=0.0001,
|
73
|
+
completion_audio=0.0002,
|
74
|
+
prompt_cached_input=0.00000125,
|
75
|
+
prompt_cached_creation=0.00000125,
|
25
76
|
)
|
26
|
-
for model, info in data.items()
|
27
|
-
if "input_cost_per_token" in info and "output_cost_per_token" in info
|
28
|
-
}
|
29
|
-
|
30
|
-
|
31
|
-
MODEL_COSTS = _get_model_costs()
|
32
|
-
|
33
|
-
|
34
|
-
def _calculate_cost(
|
35
|
-
usages: list[TokenUsage], provider: Optional[str] = None
|
36
|
-
) -> TokenUsageReport:
|
37
|
-
"""Calculate cost from token usage records."""
|
38
|
-
# Group usages by provider and model
|
39
|
-
provider_model_usages: Dict[str, Dict[str, list[TokenUsage]]] = {}
|
40
|
-
|
41
|
-
print(f"usages: {len(usages)}")
|
42
|
-
|
43
|
-
for usage in usages:
|
44
|
-
if usage.model not in MODEL_COSTS:
|
45
|
-
continue
|
46
|
-
|
47
|
-
provider = usage.provider
|
48
|
-
if provider not in provider_model_usages:
|
49
|
-
provider_model_usages[provider] = {}
|
50
|
-
|
51
|
-
if usage.model not in provider_model_usages[provider]:
|
52
|
-
provider_model_usages[provider][usage.model] = []
|
53
77
|
|
54
|
-
provider_model_usages[
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
78
|
+
provider_model_usages: Dict[str, Dict[str, list[TokenUsage]]] = {}
|
79
|
+
logger.debug(f"usages: {len(usages)}")
|
80
|
+
|
81
|
+
for usage in usages:
|
82
|
+
# Model key resolution logic (unchanged)
|
83
|
+
model_key = usage.model
|
84
|
+
if model_key in self.MODEL_COSTS:
|
85
|
+
pass
|
86
|
+
elif f"{usage.provider}/{usage.model}" in self.MODEL_COSTS:
|
87
|
+
model_key = f"{usage.provider}/{usage.model}"
|
88
|
+
else:
|
89
|
+
matched_keys = [k for k in self.MODEL_COSTS.keys() if usage.model in k]
|
90
|
+
if matched_keys:
|
91
|
+
model_key = matched_keys[0]
|
92
|
+
logger.warning(
|
93
|
+
f"Model {usage.model} matched with {model_key} in pricing data via contains search"
|
94
|
+
)
|
95
|
+
else:
|
96
|
+
logger.warning(
|
97
|
+
f"Model {model_key} not found in pricing data. Using gpt-4o pricing as fallback"
|
98
|
+
)
|
99
|
+
self.MODEL_COSTS[model_key] = GPT4O_PRICING
|
100
|
+
|
101
|
+
provider_key = usage.provider or "default"
|
102
|
+
provider_model_usages.setdefault(provider_key, {}).setdefault(
|
103
|
+
model_key, []
|
104
|
+
).append(usage)
|
105
|
+
|
106
|
+
# Calculate totals for each level
|
107
|
+
providers_list = []
|
108
|
+
total_metrics = {
|
67
109
|
"total_cost": 0.0,
|
68
110
|
"total_tokens": 0,
|
69
111
|
"prompt_tokens": 0,
|
70
112
|
"completion_tokens": 0,
|
71
113
|
}
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
114
|
+
|
115
|
+
for provider, model_usages in provider_model_usages.items():
|
116
|
+
provider_metrics = {
|
117
|
+
"total_cost": 0.0,
|
118
|
+
"total_tokens": 0,
|
119
|
+
"prompt_tokens": 0,
|
120
|
+
"completion_tokens": 0,
|
121
|
+
"prompt_cached_input_tokens": 0,
|
122
|
+
"prompt_cached_creation_tokens": 0,
|
123
|
+
"prompt_audio_tokens": 0,
|
124
|
+
"completion_audio_tokens": 0,
|
125
|
+
"completion_reasoning_tokens": 0,
|
126
|
+
"completion_accepted_prediction_tokens": 0,
|
127
|
+
"completion_rejected_prediction_tokens": 0,
|
128
|
+
}
|
129
|
+
models_list = []
|
130
|
+
|
131
|
+
for model_key, usages in model_usages.items():
|
132
|
+
model_rates = self.MODEL_COSTS[model_key]
|
133
|
+
model_cost = 0.0
|
134
|
+
model_total = 0
|
135
|
+
model_prompt = 0
|
136
|
+
model_completion = 0
|
137
|
+
|
138
|
+
for usage in usages:
|
139
|
+
# Base token costs
|
140
|
+
prompt_text_tokens = usage.prompt_tokens
|
141
|
+
if usage.prompt_cached_input_tokens:
|
142
|
+
prompt_text_tokens = (
|
143
|
+
usage.prompt_tokens - usage.prompt_cached_input_tokens
|
144
|
+
)
|
145
|
+
if usage.prompt_audio_tokens:
|
146
|
+
prompt_text_tokens = (
|
147
|
+
usage.prompt_tokens - usage.prompt_audio_tokens
|
148
|
+
)
|
149
|
+
|
150
|
+
completion_text_tokens = usage.completion_tokens
|
151
|
+
if usage.completion_audio_tokens:
|
152
|
+
completion_text_tokens = (
|
153
|
+
usage.completion_tokens - usage.completion_audio_tokens
|
154
|
+
)
|
155
|
+
|
156
|
+
prompt_cost = prompt_text_tokens * model_rates.prompt
|
157
|
+
completion_cost = completion_text_tokens * model_rates.completion
|
158
|
+
model_cost += prompt_cost + completion_cost
|
159
|
+
|
160
|
+
# Audio token costs
|
161
|
+
if usage.prompt_audio_tokens:
|
162
|
+
if model_rates.prompt_audio:
|
163
|
+
model_cost += (
|
164
|
+
usage.prompt_audio_tokens * model_rates.prompt_audio
|
165
|
+
)
|
166
|
+
else:
|
167
|
+
logger.warning(
|
168
|
+
f"Audio prompt tokens present for {model_key} but no audio rate defined"
|
169
|
+
)
|
170
|
+
|
171
|
+
if usage.completion_audio_tokens:
|
172
|
+
if model_rates.completion_audio:
|
173
|
+
model_cost += (
|
174
|
+
usage.completion_audio_tokens
|
175
|
+
* model_rates.completion_audio
|
176
|
+
)
|
177
|
+
else:
|
178
|
+
logger.warning(
|
179
|
+
f"Audio completion tokens present for {model_key} but no audio rate defined"
|
180
|
+
)
|
181
|
+
|
182
|
+
# Cached token costs
|
183
|
+
if usage.prompt_cached_input_tokens:
|
184
|
+
if model_rates.prompt_cached_input:
|
185
|
+
model_cost += (
|
186
|
+
usage.prompt_cached_input_tokens
|
187
|
+
* model_rates.prompt_cached_input
|
188
|
+
)
|
189
|
+
else:
|
190
|
+
logger.warning(
|
191
|
+
f"Cached input tokens present for {model_key} but no cache input rate defined"
|
192
|
+
)
|
193
|
+
|
194
|
+
if usage.prompt_cached_creation_tokens:
|
195
|
+
if model_rates.prompt_cached_creation:
|
196
|
+
model_cost += (
|
197
|
+
usage.prompt_cached_creation_tokens
|
198
|
+
* model_rates.prompt_cached_creation
|
199
|
+
)
|
200
|
+
else:
|
201
|
+
logger.warning(
|
202
|
+
f"Cached creation tokens present for {model_key} but no cache creation rate defined"
|
203
|
+
)
|
204
|
+
|
205
|
+
model_total += usage.total_tokens
|
206
|
+
model_prompt += usage.prompt_tokens
|
207
|
+
model_completion += usage.completion_tokens
|
208
|
+
|
209
|
+
models_list.append(
|
210
|
+
ModelUsage(
|
211
|
+
model=model_key,
|
212
|
+
total_cost=round(model_cost, 6),
|
213
|
+
total_tokens=model_total,
|
214
|
+
prompt_tokens=model_prompt,
|
215
|
+
completion_tokens=model_completion,
|
216
|
+
prompt_tokens_details=PromptTokenDetails(
|
217
|
+
cached_input_tokens=sum(
|
218
|
+
u.prompt_cached_input_tokens or 0 for u in usages
|
219
|
+
),
|
220
|
+
cached_creation_tokens=sum(
|
221
|
+
u.prompt_cached_creation_tokens or 0 for u in usages
|
222
|
+
),
|
223
|
+
audio_tokens=sum(
|
224
|
+
u.prompt_audio_tokens or 0 for u in usages
|
225
|
+
),
|
226
|
+
)
|
227
|
+
if any(
|
228
|
+
u.prompt_cached_input_tokens
|
229
|
+
or u.prompt_cached_creation_tokens
|
230
|
+
or u.prompt_audio_tokens
|
231
|
+
for u in usages
|
232
|
+
)
|
233
|
+
else None,
|
234
|
+
completion_tokens_details=CompletionTokenDetails(
|
235
|
+
audio_tokens=sum(
|
236
|
+
u.completion_audio_tokens or 0 for u in usages
|
237
|
+
),
|
238
|
+
reasoning_tokens=sum(
|
239
|
+
u.completion_reasoning_tokens or 0 for u in usages
|
240
|
+
),
|
241
|
+
accepted_prediction_tokens=sum(
|
242
|
+
u.completion_accepted_prediction_tokens or 0
|
243
|
+
for u in usages
|
244
|
+
),
|
245
|
+
rejected_prediction_tokens=sum(
|
246
|
+
u.completion_rejected_prediction_tokens or 0
|
247
|
+
for u in usages
|
248
|
+
),
|
249
|
+
)
|
250
|
+
if any(
|
251
|
+
getattr(u, attr, None)
|
252
|
+
for u in usages
|
253
|
+
for attr in [
|
254
|
+
"completion_audio_tokens",
|
255
|
+
"completion_reasoning_tokens",
|
256
|
+
"completion_accepted_prediction_tokens",
|
257
|
+
"completion_rejected_prediction_tokens",
|
258
|
+
]
|
259
|
+
)
|
260
|
+
else None,
|
261
|
+
)
|
88
262
|
)
|
89
263
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
264
|
+
# Update provider metrics with all token types
|
265
|
+
provider_metrics["total_cost"] += model_cost
|
266
|
+
provider_metrics["total_tokens"] += model_total
|
267
|
+
provider_metrics["prompt_tokens"] += model_prompt
|
268
|
+
provider_metrics["completion_tokens"] += model_completion
|
269
|
+
provider_metrics["prompt_cached_input_tokens"] += sum(
|
270
|
+
u.prompt_cached_input_tokens or 0 for u in usages
|
271
|
+
)
|
272
|
+
provider_metrics["prompt_cached_creation_tokens"] += sum(
|
273
|
+
u.prompt_cached_creation_tokens or 0 for u in usages
|
274
|
+
)
|
275
|
+
provider_metrics["prompt_audio_tokens"] += sum(
|
276
|
+
u.prompt_audio_tokens or 0 for u in usages
|
277
|
+
)
|
278
|
+
provider_metrics["completion_audio_tokens"] += sum(
|
279
|
+
u.completion_audio_tokens or 0 for u in usages
|
280
|
+
)
|
281
|
+
provider_metrics["completion_reasoning_tokens"] += sum(
|
282
|
+
u.completion_reasoning_tokens or 0 for u in usages
|
283
|
+
)
|
284
|
+
provider_metrics["completion_accepted_prediction_tokens"] += sum(
|
285
|
+
u.completion_accepted_prediction_tokens or 0 for u in usages
|
286
|
+
)
|
287
|
+
provider_metrics["completion_rejected_prediction_tokens"] += sum(
|
288
|
+
u.completion_rejected_prediction_tokens or 0 for u in usages
|
97
289
|
)
|
98
|
-
)
|
99
290
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
291
|
+
providers_list.append(
|
292
|
+
ProviderUsage(
|
293
|
+
provider=provider,
|
294
|
+
models=models_list,
|
295
|
+
total_cost=round(provider_metrics["total_cost"], 6),
|
296
|
+
total_tokens=provider_metrics["total_tokens"],
|
297
|
+
prompt_tokens=provider_metrics["prompt_tokens"],
|
298
|
+
completion_tokens=provider_metrics["completion_tokens"],
|
299
|
+
prompt_tokens_details=PromptTokenDetails(
|
300
|
+
cached_input_tokens=provider_metrics[
|
301
|
+
"prompt_cached_input_tokens"
|
302
|
+
],
|
303
|
+
cached_creation_tokens=provider_metrics[
|
304
|
+
"prompt_cached_creation_tokens"
|
305
|
+
],
|
306
|
+
audio_tokens=provider_metrics["prompt_audio_tokens"],
|
307
|
+
)
|
308
|
+
if provider_metrics["prompt_cached_input_tokens"]
|
309
|
+
or provider_metrics["prompt_cached_creation_tokens"]
|
310
|
+
or provider_metrics["prompt_audio_tokens"]
|
311
|
+
else None,
|
312
|
+
completion_tokens_details=CompletionTokenDetails(
|
313
|
+
audio_tokens=provider_metrics["completion_audio_tokens"],
|
314
|
+
reasoning_tokens=provider_metrics[
|
315
|
+
"completion_reasoning_tokens"
|
316
|
+
],
|
317
|
+
accepted_prediction_tokens=provider_metrics[
|
318
|
+
"completion_accepted_prediction_tokens"
|
319
|
+
],
|
320
|
+
rejected_prediction_tokens=provider_metrics[
|
321
|
+
"completion_rejected_prediction_tokens"
|
322
|
+
],
|
323
|
+
)
|
324
|
+
if any(
|
325
|
+
provider_metrics[k]
|
326
|
+
for k in [
|
327
|
+
"completion_audio_tokens",
|
328
|
+
"completion_reasoning_tokens",
|
329
|
+
"completion_accepted_prediction_tokens",
|
330
|
+
"completion_rejected_prediction_tokens",
|
331
|
+
]
|
332
|
+
)
|
333
|
+
else None,
|
334
|
+
)
|
114
335
|
)
|
115
|
-
)
|
116
|
-
|
117
|
-
# Add to grand totals
|
118
|
-
for key in total_metrics:
|
119
|
-
total_metrics[key] += provider_metrics[key]
|
120
|
-
|
121
|
-
return TokenUsageReport(
|
122
|
-
providers=providers_list,
|
123
|
-
**{
|
124
|
-
k: (round(v, 6) if k == "total_cost" else v)
|
125
|
-
for k, v in total_metrics.items()
|
126
|
-
},
|
127
|
-
)
|
128
|
-
|
129
|
-
|
130
|
-
def _query_usage(
|
131
|
-
start_date: datetime,
|
132
|
-
end_date: datetime,
|
133
|
-
provider: Optional[str] = None,
|
134
|
-
model: Optional[str] = None,
|
135
|
-
) -> TokenUsageReport:
|
136
|
-
"""Query token usage for a specific time period."""
|
137
|
-
session = get_session()()
|
138
|
-
try:
|
139
|
-
query = session.query(TokenUsage).filter(
|
140
|
-
TokenUsage.created_at.between(start_date, end_date)
|
141
|
-
)
|
142
336
|
|
143
|
-
|
144
|
-
|
145
|
-
if model:
|
146
|
-
query = query.filter(TokenUsage.model == model)
|
147
|
-
|
148
|
-
usages = query.all()
|
149
|
-
return _calculate_cost(usages, provider or "all")
|
150
|
-
finally:
|
151
|
-
session.close()
|
152
|
-
|
153
|
-
|
154
|
-
def last_hour(
|
155
|
-
provider: Optional[str] = None, model: Optional[str] = None
|
156
|
-
) -> TokenUsageReport:
|
157
|
-
"""Get cost analysis for the last hour."""
|
158
|
-
logger.debug(
|
159
|
-
f"Getting cost analysis for last hour (provider={provider}, model={model})"
|
160
|
-
)
|
161
|
-
end = datetime.now()
|
162
|
-
start = end - timedelta(hours=1)
|
163
|
-
return _query_usage(start, end, provider, model)
|
164
|
-
|
165
|
-
|
166
|
-
def last_day(
|
167
|
-
provider: Optional[str] = None, model: Optional[str] = None
|
168
|
-
) -> TokenUsageReport:
|
169
|
-
"""Get cost analysis for the last 24 hours."""
|
170
|
-
logger.debug(
|
171
|
-
f"Getting cost analysis for last 24 hours (provider={provider}, model={model})"
|
172
|
-
)
|
173
|
-
end = datetime.now()
|
174
|
-
start = end - timedelta(days=1)
|
175
|
-
return _query_usage(start, end, provider, model)
|
176
|
-
|
177
|
-
|
178
|
-
def last_week(
|
179
|
-
provider: Optional[str] = None, model: Optional[str] = None
|
180
|
-
) -> TokenUsageReport:
|
181
|
-
"""Get cost analysis for the last 7 days."""
|
182
|
-
logger.debug(
|
183
|
-
f"Getting cost analysis for last 7 days (provider={provider}, model={model})"
|
184
|
-
)
|
185
|
-
end = datetime.now()
|
186
|
-
start = end - timedelta(weeks=1)
|
187
|
-
return _query_usage(start, end, provider, model)
|
188
|
-
|
189
|
-
|
190
|
-
def last_month(
|
191
|
-
provider: Optional[str] = None, model: Optional[str] = None
|
192
|
-
) -> TokenUsageReport:
|
193
|
-
"""Get cost analysis for the last 30 days."""
|
194
|
-
logger.debug(
|
195
|
-
f"Getting cost analysis for last 30 days (provider={provider}, model={model})"
|
196
|
-
)
|
197
|
-
end = datetime.now()
|
198
|
-
start = end - timedelta(days=30)
|
199
|
-
return _query_usage(start, end, provider, model)
|
200
|
-
|
201
|
-
|
202
|
-
def between(
|
203
|
-
start_date: Union[datetime, str],
|
204
|
-
end_date: Union[datetime, str],
|
205
|
-
provider: Optional[str] = None,
|
206
|
-
model: Optional[str] = None,
|
207
|
-
) -> TokenUsageReport:
|
208
|
-
"""Get cost analysis between two dates.
|
209
|
-
|
210
|
-
Args:
|
211
|
-
start_date: datetime object or string (format: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)
|
212
|
-
end_date: datetime object or string (format: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS)
|
213
|
-
"""
|
214
|
-
logger.debug(
|
215
|
-
f"Getting cost analysis between {start_date} and {end_date} (provider={provider}, model={model})"
|
216
|
-
)
|
217
|
-
|
218
|
-
if isinstance(start_date, str):
|
219
|
-
try:
|
220
|
-
start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
|
221
|
-
except ValueError:
|
222
|
-
logger.warning(
|
223
|
-
f"Date-only string provided for start_date: {start_date}. Setting time to 00:00:00"
|
224
|
-
)
|
225
|
-
start = datetime.strptime(start_date, "%Y-%m-%d")
|
337
|
+
for key in total_metrics:
|
338
|
+
total_metrics[key] += provider_metrics[key]
|
226
339
|
|
227
|
-
|
228
|
-
|
340
|
+
return TokenUsageReport(
|
341
|
+
providers=providers_list,
|
342
|
+
**{
|
343
|
+
k: (round(v, 6) if k == "total_cost" else v)
|
344
|
+
for k, v in total_metrics.items()
|
345
|
+
},
|
346
|
+
)
|
229
347
|
|
230
|
-
|
348
|
+
def _query_usage(
|
349
|
+
self,
|
350
|
+
start_date: datetime,
|
351
|
+
end_date: datetime,
|
352
|
+
provider: Optional[str] = None,
|
353
|
+
model: Optional[str] = None,
|
354
|
+
) -> TokenUsageReport:
|
355
|
+
if not state.is_tokenator_enabled:
|
356
|
+
logger.warning("Tokenator is disabled. Skipping usage query.")
|
357
|
+
return TokenUsageReport()
|
358
|
+
|
359
|
+
session = get_session()()
|
231
360
|
try:
|
232
|
-
|
233
|
-
|
234
|
-
logger.warning(
|
235
|
-
f"Date-only string provided for end_date: {end_date}. Setting time to 23:59:59"
|
236
|
-
)
|
237
|
-
end = (
|
238
|
-
datetime.strptime(end_date, "%Y-%m-%d")
|
239
|
-
+ timedelta(days=1)
|
240
|
-
- timedelta(seconds=1)
|
361
|
+
query = session.query(TokenUsage).filter(
|
362
|
+
TokenUsage.created_at.between(start_date, end_date)
|
241
363
|
)
|
242
|
-
else:
|
243
|
-
end = end_date
|
244
364
|
|
245
|
-
|
365
|
+
if provider:
|
366
|
+
query = query.filter(TokenUsage.provider == provider)
|
367
|
+
if model:
|
368
|
+
query = query.filter(TokenUsage.model == model)
|
246
369
|
|
370
|
+
usages = query.all()
|
247
371
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
session = get_session()()
|
252
|
-
query = session.query(TokenUsage).filter(TokenUsage.execution_id == execution_id)
|
253
|
-
return _calculate_cost(query.all())
|
372
|
+
return self._calculate_cost(usages, provider or "all")
|
373
|
+
finally:
|
374
|
+
session.close()
|
254
375
|
|
376
|
+
def last_hour(
|
377
|
+
self, provider: Optional[str] = None, model: Optional[str] = None
|
378
|
+
) -> TokenUsageReport:
|
379
|
+
if not state.is_tokenator_enabled:
|
380
|
+
return TokenUsageReport()
|
381
|
+
logger.debug(
|
382
|
+
f"Getting cost analysis for last hour (provider={provider}, model={model})"
|
383
|
+
)
|
384
|
+
end = datetime.now()
|
385
|
+
start = end - timedelta(hours=1)
|
386
|
+
return self._query_usage(start, end, provider, model)
|
387
|
+
|
388
|
+
def last_day(
|
389
|
+
self, provider: Optional[str] = None, model: Optional[str] = None
|
390
|
+
) -> TokenUsageReport:
|
391
|
+
if not state.is_tokenator_enabled:
|
392
|
+
return TokenUsageReport()
|
393
|
+
logger.debug(
|
394
|
+
f"Getting cost analysis for last 24 hours (provider={provider}, model={model})"
|
395
|
+
)
|
396
|
+
end = datetime.now()
|
397
|
+
start = end - timedelta(days=1)
|
398
|
+
return self._query_usage(start, end, provider, model)
|
399
|
+
|
400
|
+
def last_week(
|
401
|
+
self, provider: Optional[str] = None, model: Optional[str] = None
|
402
|
+
) -> TokenUsageReport:
|
403
|
+
if not state.is_tokenator_enabled:
|
404
|
+
return TokenUsageReport()
|
405
|
+
logger.debug(
|
406
|
+
f"Getting cost analysis for last 7 days (provider={provider}, model={model})"
|
407
|
+
)
|
408
|
+
end = datetime.now()
|
409
|
+
start = end - timedelta(weeks=1)
|
410
|
+
return self._query_usage(start, end, provider, model)
|
411
|
+
|
412
|
+
def last_month(
|
413
|
+
self, provider: Optional[str] = None, model: Optional[str] = None
|
414
|
+
) -> TokenUsageReport:
|
415
|
+
if not state.is_tokenator_enabled:
|
416
|
+
return TokenUsageReport()
|
417
|
+
logger.debug(
|
418
|
+
f"Getting cost analysis for last 30 days (provider={provider}, model={model})"
|
419
|
+
)
|
420
|
+
end = datetime.now()
|
421
|
+
start = end - timedelta(days=30)
|
422
|
+
return self._query_usage(start, end, provider, model)
|
423
|
+
|
424
|
+
def between(
|
425
|
+
self,
|
426
|
+
start_date: Union[datetime, str],
|
427
|
+
end_date: Union[datetime, str],
|
428
|
+
provider: Optional[str] = None,
|
429
|
+
model: Optional[str] = None,
|
430
|
+
) -> TokenUsageReport:
|
431
|
+
if not state.is_tokenator_enabled:
|
432
|
+
return TokenUsageReport()
|
433
|
+
logger.debug(
|
434
|
+
f"Getting cost analysis between {start_date} and {end_date} (provider={provider}, model={model})"
|
435
|
+
)
|
255
436
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
437
|
+
if isinstance(start_date, str):
|
438
|
+
try:
|
439
|
+
start = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
|
440
|
+
except ValueError:
|
441
|
+
logger.warning(
|
442
|
+
f"Date-only string provided for start_date: {start_date}. Setting time to 00:00:00"
|
443
|
+
)
|
444
|
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
445
|
+
else:
|
446
|
+
start = start_date
|
447
|
+
|
448
|
+
if isinstance(end_date, str):
|
449
|
+
try:
|
450
|
+
end = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S")
|
451
|
+
except ValueError:
|
452
|
+
logger.warning(
|
453
|
+
f"Date-only string provided for end_date: {end_date}. Setting time to 23:59:59"
|
454
|
+
)
|
455
|
+
end = (
|
456
|
+
datetime.strptime(end_date, "%Y-%m-%d")
|
457
|
+
+ timedelta(days=1)
|
458
|
+
- timedelta(seconds=1)
|
459
|
+
)
|
460
|
+
else:
|
461
|
+
end = end_date
|
262
462
|
|
463
|
+
return self._query_usage(start, end, provider, model)
|
263
464
|
|
264
|
-
def
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
465
|
+
def for_execution(self, execution_id: str) -> TokenUsageReport:
|
466
|
+
if not state.is_tokenator_enabled:
|
467
|
+
return TokenUsageReport()
|
468
|
+
logger.debug(f"Getting cost analysis for execution_id={execution_id}")
|
469
|
+
session = get_session()()
|
470
|
+
try:
|
471
|
+
query = session.query(TokenUsage).filter(
|
472
|
+
TokenUsage.execution_id == execution_id
|
473
|
+
)
|
474
|
+
return self._calculate_cost(query.all())
|
475
|
+
finally:
|
476
|
+
session.close()
|
477
|
+
|
478
|
+
def last_execution(self) -> TokenUsageReport:
|
479
|
+
if not state.is_tokenator_enabled:
|
480
|
+
return TokenUsageReport()
|
481
|
+
logger.debug("Getting cost analysis for last execution")
|
482
|
+
session = get_session()()
|
483
|
+
try:
|
484
|
+
query = (
|
485
|
+
session.query(TokenUsage).order_by(TokenUsage.created_at.desc()).first()
|
486
|
+
)
|
487
|
+
if query:
|
488
|
+
return self.for_execution(query.execution_id)
|
489
|
+
return TokenUsageReport()
|
490
|
+
finally:
|
491
|
+
session.close()
|
492
|
+
|
493
|
+
def all_time(self) -> TokenUsageReport:
|
494
|
+
if not state.is_tokenator_enabled:
|
495
|
+
return TokenUsageReport()
|
496
|
+
|
497
|
+
logger.warning("Getting cost analysis for all time. This may take a while...")
|
498
|
+
session = get_session()()
|
499
|
+
try:
|
500
|
+
query = session.query(TokenUsage)
|
501
|
+
return self._calculate_cost(query.all())
|
502
|
+
finally:
|
503
|
+
session.close()
|