gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.6__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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/classification/__init__.py +31 -0
- gitflow_analytics/classification/batch_classifier.py +752 -0
- gitflow_analytics/classification/classifier.py +464 -0
- gitflow_analytics/classification/feature_extractor.py +725 -0
- gitflow_analytics/classification/linguist_analyzer.py +574 -0
- gitflow_analytics/classification/model.py +455 -0
- gitflow_analytics/cli.py +4108 -350
- gitflow_analytics/cli_rich.py +198 -48
- gitflow_analytics/config/__init__.py +43 -0
- gitflow_analytics/config/errors.py +261 -0
- gitflow_analytics/config/loader.py +904 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +441 -0
- gitflow_analytics/config/validator.py +154 -0
- gitflow_analytics/config.py +44 -508
- gitflow_analytics/core/analyzer.py +1209 -98
- gitflow_analytics/core/cache.py +1337 -29
- gitflow_analytics/core/data_fetcher.py +1193 -0
- gitflow_analytics/core/identity.py +363 -14
- gitflow_analytics/core/metrics_storage.py +526 -0
- gitflow_analytics/core/progress.py +372 -0
- gitflow_analytics/core/schema_version.py +269 -0
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +8 -1
- gitflow_analytics/extractors/tickets.py +749 -11
- gitflow_analytics/identity_llm/__init__.py +6 -0
- gitflow_analytics/identity_llm/analysis_pass.py +231 -0
- gitflow_analytics/identity_llm/analyzer.py +464 -0
- gitflow_analytics/identity_llm/models.py +76 -0
- gitflow_analytics/integrations/github_integration.py +175 -11
- gitflow_analytics/integrations/jira_integration.py +461 -24
- gitflow_analytics/integrations/orchestrator.py +124 -1
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +379 -20
- gitflow_analytics/models/database.py +843 -53
- gitflow_analytics/pm_framework/__init__.py +115 -0
- gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
- gitflow_analytics/pm_framework/base.py +406 -0
- gitflow_analytics/pm_framework/models.py +211 -0
- gitflow_analytics/pm_framework/orchestrator.py +652 -0
- gitflow_analytics/pm_framework/registry.py +333 -0
- gitflow_analytics/qualitative/__init__.py +9 -10
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
- gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
- gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
- gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
- gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
- gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
- gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
- gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
- gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
- gitflow_analytics/qualitative/core/__init__.py +4 -4
- gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
- gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
- gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
- gitflow_analytics/qualitative/core/processor.py +381 -248
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +7 -7
- gitflow_analytics/qualitative/models/schemas.py +155 -121
- gitflow_analytics/qualitative/utils/__init__.py +4 -4
- gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
- gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
- gitflow_analytics/qualitative/utils/metrics.py +172 -158
- gitflow_analytics/qualitative/utils/text_processing.py +146 -104
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +539 -14
- gitflow_analytics/reports/base.py +648 -0
- gitflow_analytics/reports/branch_health_writer.py +322 -0
- gitflow_analytics/reports/classification_writer.py +924 -0
- gitflow_analytics/reports/cli_integration.py +427 -0
- gitflow_analytics/reports/csv_writer.py +1676 -212
- gitflow_analytics/reports/data_models.py +504 -0
- gitflow_analytics/reports/database_report_generator.py +427 -0
- gitflow_analytics/reports/example_usage.py +344 -0
- gitflow_analytics/reports/factory.py +499 -0
- gitflow_analytics/reports/formatters.py +698 -0
- gitflow_analytics/reports/html_generator.py +1116 -0
- gitflow_analytics/reports/interfaces.py +489 -0
- gitflow_analytics/reports/json_exporter.py +2770 -0
- gitflow_analytics/reports/narrative_writer.py +2287 -158
- gitflow_analytics/reports/story_point_correlation.py +1144 -0
- gitflow_analytics/reports/weekly_trends_writer.py +389 -0
- gitflow_analytics/training/__init__.py +5 -0
- gitflow_analytics/training/model_loader.py +377 -0
- gitflow_analytics/training/pipeline.py +550 -0
- gitflow_analytics/tui/__init__.py +1 -1
- gitflow_analytics/tui/app.py +129 -126
- gitflow_analytics/tui/screens/__init__.py +3 -3
- gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
- gitflow_analytics/tui/screens/configuration_screen.py +154 -178
- gitflow_analytics/tui/screens/loading_screen.py +100 -110
- gitflow_analytics/tui/screens/main_screen.py +89 -72
- gitflow_analytics/tui/screens/results_screen.py +305 -281
- gitflow_analytics/tui/widgets/__init__.py +2 -2
- gitflow_analytics/tui/widgets/data_table.py +67 -69
- gitflow_analytics/tui/widgets/export_modal.py +76 -76
- gitflow_analytics/tui/widgets/progress_widget.py +41 -46
- gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
- gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"""Cost tracking utilities for LLM usage monitoring."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
4
|
-
from datetime import datetime, timedelta
|
|
5
|
-
from typing import Dict, List, Optional
|
|
6
5
|
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
import
|
|
8
|
+
from typing import Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class LLMCall:
|
|
13
13
|
"""Record of a single LLM API call."""
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
timestamp: datetime
|
|
16
16
|
model: str
|
|
17
17
|
input_tokens: int
|
|
@@ -25,36 +25,33 @@ class LLMCall:
|
|
|
25
25
|
|
|
26
26
|
class CostTracker:
|
|
27
27
|
"""Track and manage LLM API usage costs.
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
This class provides cost monitoring, budgeting, and optimization
|
|
30
30
|
features to keep LLM usage within acceptable limits while
|
|
31
31
|
maintaining analysis quality.
|
|
32
32
|
"""
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
# OpenRouter pricing (approximate, in USD per 1M tokens)
|
|
35
35
|
MODEL_PRICING = {
|
|
36
36
|
# Anthropic models
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
"anthropic/claude-3-haiku": {"input": 0.25, "output": 1.25},
|
|
38
|
+
"anthropic/claude-3-sonnet": {"input": 3.0, "output": 15.0},
|
|
39
|
+
"anthropic/claude-3-opus": {"input": 15.0, "output": 75.0},
|
|
41
40
|
# OpenAI models
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
"openai/gpt-3.5-turbo": {"input": 0.5, "output": 1.5},
|
|
42
|
+
"openai/gpt-4": {"input": 30.0, "output": 60.0},
|
|
43
|
+
"openai/gpt-4-turbo": {"input": 10.0, "output": 30.0},
|
|
46
44
|
# Free models (Llama)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
"meta-llama/llama-3.1-8b-instruct:free": {"input": 0.0, "output": 0.0},
|
|
46
|
+
"meta-llama/llama-3.1-70b-instruct:free": {"input": 0.0, "output": 0.0},
|
|
50
47
|
# Other popular models
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
"google/gemini-pro": {"input": 0.5, "output": 1.5},
|
|
49
|
+
"mistralai/mixtral-8x7b-instruct": {"input": 0.27, "output": 0.27},
|
|
53
50
|
}
|
|
54
|
-
|
|
51
|
+
|
|
55
52
|
def __init__(self, cache_dir: Optional[Path] = None, daily_budget: float = 5.0):
|
|
56
53
|
"""Initialize cost tracker.
|
|
57
|
-
|
|
54
|
+
|
|
58
55
|
Args:
|
|
59
56
|
cache_dir: Directory to store cost tracking data
|
|
60
57
|
daily_budget: Maximum daily spending in USD
|
|
@@ -62,33 +59,40 @@ class CostTracker:
|
|
|
62
59
|
self.daily_budget = daily_budget
|
|
63
60
|
self.cache_dir = cache_dir or Path(".qualitative_cache")
|
|
64
61
|
self.cache_dir.mkdir(exist_ok=True)
|
|
65
|
-
|
|
62
|
+
|
|
66
63
|
self.cost_file = self.cache_dir / "llm_costs.json"
|
|
67
|
-
self.calls:
|
|
64
|
+
self.calls: list[LLMCall] = []
|
|
68
65
|
self.logger = logging.getLogger(__name__)
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
# Load existing cost data
|
|
71
68
|
self._load_cost_data()
|
|
72
|
-
|
|
73
|
-
def record_call(
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
|
|
70
|
+
def record_call(
|
|
71
|
+
self,
|
|
72
|
+
model: str,
|
|
73
|
+
input_tokens: int,
|
|
74
|
+
output_tokens: int,
|
|
75
|
+
processing_time: float,
|
|
76
|
+
batch_size: int = 1,
|
|
77
|
+
success: bool = True,
|
|
78
|
+
error_message: Optional[str] = None,
|
|
79
|
+
) -> float:
|
|
76
80
|
"""Record an LLM API call and return estimated cost.
|
|
77
|
-
|
|
81
|
+
|
|
78
82
|
Args:
|
|
79
83
|
model: Model name used
|
|
80
84
|
input_tokens: Number of input tokens
|
|
81
|
-
output_tokens: Number of output tokens
|
|
85
|
+
output_tokens: Number of output tokens
|
|
82
86
|
processing_time: Processing time in seconds
|
|
83
87
|
batch_size: Number of commits processed in this call
|
|
84
88
|
success: Whether the call was successful
|
|
85
89
|
error_message: Error message if call failed
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
Returns:
|
|
88
92
|
Estimated cost in USD
|
|
89
93
|
"""
|
|
90
94
|
estimated_cost = self._calculate_cost(model, input_tokens, output_tokens)
|
|
91
|
-
|
|
95
|
+
|
|
92
96
|
call = LLMCall(
|
|
93
97
|
timestamp=datetime.utcnow(),
|
|
94
98
|
model=model,
|
|
@@ -98,174 +102,172 @@ class CostTracker:
|
|
|
98
102
|
estimated_cost=estimated_cost,
|
|
99
103
|
batch_size=batch_size,
|
|
100
104
|
success=success,
|
|
101
|
-
error_message=error_message
|
|
105
|
+
error_message=error_message,
|
|
102
106
|
)
|
|
103
|
-
|
|
107
|
+
|
|
104
108
|
self.calls.append(call)
|
|
105
109
|
self._save_cost_data()
|
|
106
|
-
|
|
110
|
+
|
|
107
111
|
# Log cost information
|
|
108
112
|
self.logger.info(
|
|
109
113
|
f"LLM call: {model} | tokens: {input_tokens}+{output_tokens} | "
|
|
110
114
|
f"cost: ${estimated_cost:.4f} | batch: {batch_size}"
|
|
111
115
|
)
|
|
112
|
-
|
|
116
|
+
|
|
113
117
|
return estimated_cost
|
|
114
|
-
|
|
118
|
+
|
|
115
119
|
def get_daily_spend(self, date: Optional[datetime] = None) -> float:
|
|
116
120
|
"""Get total spending for a specific date.
|
|
117
|
-
|
|
121
|
+
|
|
118
122
|
Args:
|
|
119
123
|
date: Date to check (defaults to today)
|
|
120
|
-
|
|
124
|
+
|
|
121
125
|
Returns:
|
|
122
126
|
Total spending in USD for the date
|
|
123
127
|
"""
|
|
124
128
|
if date is None:
|
|
125
129
|
date = datetime.utcnow()
|
|
126
|
-
|
|
130
|
+
|
|
127
131
|
start_of_day = date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
128
132
|
end_of_day = start_of_day + timedelta(days=1)
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
daily_spend = sum(
|
|
131
|
-
call.estimated_cost
|
|
135
|
+
call.estimated_cost
|
|
136
|
+
for call in self.calls
|
|
132
137
|
if start_of_day <= call.timestamp < end_of_day and call.success
|
|
133
138
|
)
|
|
134
|
-
|
|
139
|
+
|
|
135
140
|
return daily_spend
|
|
136
|
-
|
|
141
|
+
|
|
137
142
|
def check_budget_remaining(self) -> float:
|
|
138
143
|
"""Check remaining budget for today.
|
|
139
|
-
|
|
144
|
+
|
|
140
145
|
Returns:
|
|
141
146
|
Remaining budget in USD (negative if over budget)
|
|
142
147
|
"""
|
|
143
148
|
daily_spend = self.get_daily_spend()
|
|
144
149
|
return self.daily_budget - daily_spend
|
|
145
|
-
|
|
150
|
+
|
|
146
151
|
def can_afford_call(self, model: str, estimated_tokens: int) -> bool:
|
|
147
152
|
"""Check if we can afford an API call within budget.
|
|
148
|
-
|
|
153
|
+
|
|
149
154
|
Args:
|
|
150
155
|
model: Model to use
|
|
151
156
|
estimated_tokens: Estimated total tokens (input + output)
|
|
152
|
-
|
|
157
|
+
|
|
153
158
|
Returns:
|
|
154
159
|
True if call is within budget
|
|
155
160
|
"""
|
|
156
161
|
estimated_cost = self._calculate_cost(model, estimated_tokens // 2, estimated_tokens // 2)
|
|
157
162
|
remaining_budget = self.check_budget_remaining()
|
|
158
|
-
|
|
163
|
+
|
|
159
164
|
return remaining_budget >= estimated_cost
|
|
160
|
-
|
|
161
|
-
def get_usage_stats(self, days: int = 7) ->
|
|
165
|
+
|
|
166
|
+
def get_usage_stats(self, days: int = 7) -> dict[str, any]:
|
|
162
167
|
"""Get usage statistics for the last N days.
|
|
163
|
-
|
|
168
|
+
|
|
164
169
|
Args:
|
|
165
170
|
days: Number of days to analyze
|
|
166
|
-
|
|
171
|
+
|
|
167
172
|
Returns:
|
|
168
173
|
Dictionary with usage statistics
|
|
169
174
|
"""
|
|
170
175
|
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
171
176
|
recent_calls = [call for call in self.calls if call.timestamp >= cutoff_date]
|
|
172
|
-
|
|
177
|
+
|
|
173
178
|
if not recent_calls:
|
|
174
179
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
"total_calls": 0,
|
|
181
|
+
"total_cost": 0.0,
|
|
182
|
+
"total_tokens": 0,
|
|
183
|
+
"avg_cost_per_call": 0.0,
|
|
184
|
+
"model_usage": {},
|
|
185
|
+
"success_rate": 1.0,
|
|
181
186
|
}
|
|
182
|
-
|
|
187
|
+
|
|
183
188
|
successful_calls = [call for call in recent_calls if call.success]
|
|
184
|
-
|
|
189
|
+
|
|
185
190
|
# Calculate statistics
|
|
186
191
|
total_cost = sum(call.estimated_cost for call in successful_calls)
|
|
187
192
|
total_tokens = sum(call.input_tokens + call.output_tokens for call in recent_calls)
|
|
188
|
-
|
|
193
|
+
|
|
189
194
|
# Model usage breakdown
|
|
190
195
|
model_usage = {}
|
|
191
196
|
for call in recent_calls:
|
|
192
197
|
if call.model not in model_usage:
|
|
193
|
-
model_usage[call.model] = {
|
|
194
|
-
model_usage[call.model][
|
|
195
|
-
model_usage[call.model][
|
|
196
|
-
model_usage[call.model][
|
|
197
|
-
|
|
198
|
+
model_usage[call.model] = {"calls": 0, "cost": 0.0, "tokens": 0}
|
|
199
|
+
model_usage[call.model]["calls"] += 1
|
|
200
|
+
model_usage[call.model]["cost"] += call.estimated_cost
|
|
201
|
+
model_usage[call.model]["tokens"] += call.input_tokens + call.output_tokens
|
|
202
|
+
|
|
198
203
|
return {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
"total_calls": len(recent_calls),
|
|
205
|
+
"successful_calls": len(successful_calls),
|
|
206
|
+
"total_cost": total_cost,
|
|
207
|
+
"total_tokens": total_tokens,
|
|
208
|
+
"avg_cost_per_call": total_cost / len(successful_calls) if successful_calls else 0.0,
|
|
209
|
+
"model_usage": model_usage,
|
|
210
|
+
"success_rate": len(successful_calls) / len(recent_calls) if recent_calls else 1.0,
|
|
211
|
+
"daily_average_cost": total_cost / days,
|
|
207
212
|
}
|
|
208
|
-
|
|
209
|
-
def suggest_cost_optimizations(self) ->
|
|
213
|
+
|
|
214
|
+
def suggest_cost_optimizations(self) -> list[str]:
|
|
210
215
|
"""Suggest ways to optimize costs based on usage patterns.
|
|
211
|
-
|
|
216
|
+
|
|
212
217
|
Returns:
|
|
213
218
|
List of optimization suggestions
|
|
214
219
|
"""
|
|
215
220
|
suggestions = []
|
|
216
221
|
stats = self.get_usage_stats(days=7)
|
|
217
|
-
|
|
218
|
-
if stats[
|
|
222
|
+
|
|
223
|
+
if stats["total_calls"] == 0:
|
|
219
224
|
return suggestions
|
|
220
|
-
|
|
225
|
+
|
|
221
226
|
# Check if expensive models are overused
|
|
222
|
-
model_usage = stats[
|
|
223
|
-
total_cost = stats[
|
|
224
|
-
|
|
225
|
-
expensive_models = [
|
|
227
|
+
model_usage = stats["model_usage"]
|
|
228
|
+
total_cost = stats["total_cost"]
|
|
229
|
+
|
|
230
|
+
expensive_models = ["anthropic/claude-3-opus", "openai/gpt-4"]
|
|
226
231
|
expensive_usage = sum(
|
|
227
|
-
model_usage.get(model, {}).get(
|
|
228
|
-
for model in expensive_models
|
|
232
|
+
model_usage.get(model, {}).get("cost", 0) for model in expensive_models
|
|
229
233
|
)
|
|
230
|
-
|
|
234
|
+
|
|
231
235
|
if expensive_usage > total_cost * 0.3:
|
|
232
236
|
suggestions.append(
|
|
233
237
|
"Consider using cheaper models (Claude Haiku, GPT-3.5) for routine classification"
|
|
234
238
|
)
|
|
235
|
-
|
|
239
|
+
|
|
236
240
|
# Check for free model opportunities
|
|
237
|
-
free_usage = model_usage.get(
|
|
238
|
-
if free_usage < stats[
|
|
241
|
+
free_usage = model_usage.get("meta-llama/llama-3.1-8b-instruct:free", {}).get("calls", 0)
|
|
242
|
+
if free_usage < stats["total_calls"] * 0.5:
|
|
239
243
|
suggestions.append(
|
|
240
244
|
"Increase usage of free Llama models for simple classification tasks"
|
|
241
245
|
)
|
|
242
|
-
|
|
246
|
+
|
|
243
247
|
# Check daily spend
|
|
244
248
|
if self.get_daily_spend() > self.daily_budget * 0.8:
|
|
245
249
|
suggestions.append(
|
|
246
250
|
"Approaching daily budget limit - consider increasing NLP confidence threshold"
|
|
247
251
|
)
|
|
248
|
-
|
|
252
|
+
|
|
249
253
|
# Check batch efficiency
|
|
250
|
-
avg_batch_size = sum(
|
|
251
|
-
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
+
avg_batch_size = sum(call.batch_size for call in self.calls[-50:]) / min( # Last 50 calls
|
|
255
|
+
50, len(self.calls)
|
|
256
|
+
)
|
|
257
|
+
|
|
254
258
|
if avg_batch_size < 3:
|
|
255
|
-
suggestions.append(
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
+
suggestions.append("Increase batch size for LLM calls to improve cost efficiency")
|
|
260
|
+
|
|
259
261
|
return suggestions
|
|
260
|
-
|
|
262
|
+
|
|
261
263
|
def _calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
|
|
262
264
|
"""Calculate estimated cost for an API call.
|
|
263
|
-
|
|
265
|
+
|
|
264
266
|
Args:
|
|
265
267
|
model: Model name
|
|
266
268
|
input_tokens: Number of input tokens
|
|
267
269
|
output_tokens: Number of output tokens
|
|
268
|
-
|
|
270
|
+
|
|
269
271
|
Returns:
|
|
270
272
|
Estimated cost in USD
|
|
271
273
|
"""
|
|
@@ -276,68 +278,68 @@ class CostTracker:
|
|
|
276
278
|
self.logger.warning(f"Unknown model pricing for {model}, using default rates")
|
|
277
279
|
else:
|
|
278
280
|
pricing = self.MODEL_PRICING[model]
|
|
279
|
-
input_price = pricing[
|
|
280
|
-
output_price = pricing[
|
|
281
|
-
|
|
281
|
+
input_price = pricing["input"]
|
|
282
|
+
output_price = pricing["output"]
|
|
283
|
+
|
|
282
284
|
# Calculate cost (pricing is per 1M tokens)
|
|
283
285
|
input_cost = (input_tokens / 1_000_000) * input_price
|
|
284
286
|
output_cost = (output_tokens / 1_000_000) * output_price
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
return input_cost + output_cost
|
|
287
|
-
|
|
289
|
+
|
|
288
290
|
def _load_cost_data(self) -> None:
|
|
289
291
|
"""Load cost tracking data from file."""
|
|
290
292
|
if not self.cost_file.exists():
|
|
291
293
|
return
|
|
292
|
-
|
|
294
|
+
|
|
293
295
|
try:
|
|
294
|
-
with open(self.cost_file
|
|
296
|
+
with open(self.cost_file) as f:
|
|
295
297
|
data = json.load(f)
|
|
296
|
-
|
|
298
|
+
|
|
297
299
|
self.calls = []
|
|
298
|
-
for call_data in data.get(
|
|
300
|
+
for call_data in data.get("calls", []):
|
|
299
301
|
call = LLMCall(
|
|
300
|
-
timestamp=datetime.fromisoformat(call_data[
|
|
301
|
-
model=call_data[
|
|
302
|
-
input_tokens=call_data[
|
|
303
|
-
output_tokens=call_data[
|
|
304
|
-
processing_time_ms=call_data[
|
|
305
|
-
estimated_cost=call_data[
|
|
306
|
-
batch_size=call_data.get(
|
|
307
|
-
success=call_data.get(
|
|
308
|
-
error_message=call_data.get(
|
|
302
|
+
timestamp=datetime.fromisoformat(call_data["timestamp"]),
|
|
303
|
+
model=call_data["model"],
|
|
304
|
+
input_tokens=call_data["input_tokens"],
|
|
305
|
+
output_tokens=call_data["output_tokens"],
|
|
306
|
+
processing_time_ms=call_data["processing_time_ms"],
|
|
307
|
+
estimated_cost=call_data["estimated_cost"],
|
|
308
|
+
batch_size=call_data.get("batch_size", 1),
|
|
309
|
+
success=call_data.get("success", True),
|
|
310
|
+
error_message=call_data.get("error_message"),
|
|
309
311
|
)
|
|
310
312
|
self.calls.append(call)
|
|
311
|
-
|
|
313
|
+
|
|
312
314
|
except Exception as e:
|
|
313
315
|
self.logger.error(f"Failed to load cost data: {e}")
|
|
314
316
|
self.calls = []
|
|
315
|
-
|
|
317
|
+
|
|
316
318
|
def _save_cost_data(self) -> None:
|
|
317
319
|
"""Save cost tracking data to file."""
|
|
318
320
|
try:
|
|
319
321
|
# Keep only last 1000 calls to prevent file from growing too large
|
|
320
322
|
recent_calls = self.calls[-1000:]
|
|
321
|
-
|
|
323
|
+
|
|
322
324
|
data = {
|
|
323
|
-
|
|
325
|
+
"calls": [
|
|
324
326
|
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
327
|
+
"timestamp": call.timestamp.isoformat(),
|
|
328
|
+
"model": call.model,
|
|
329
|
+
"input_tokens": call.input_tokens,
|
|
330
|
+
"output_tokens": call.output_tokens,
|
|
331
|
+
"processing_time_ms": call.processing_time_ms,
|
|
332
|
+
"estimated_cost": call.estimated_cost,
|
|
333
|
+
"batch_size": call.batch_size,
|
|
334
|
+
"success": call.success,
|
|
335
|
+
"error_message": call.error_message,
|
|
334
336
|
}
|
|
335
337
|
for call in recent_calls
|
|
336
338
|
]
|
|
337
339
|
}
|
|
338
|
-
|
|
339
|
-
with open(self.cost_file,
|
|
340
|
+
|
|
341
|
+
with open(self.cost_file, "w") as f:
|
|
340
342
|
json.dump(data, f, indent=2)
|
|
341
|
-
|
|
343
|
+
|
|
342
344
|
except Exception as e:
|
|
343
|
-
self.logger.error(f"Failed to save cost data: {e}")
|
|
345
|
+
self.logger.error(f"Failed to save cost data: {e}")
|