gitflow-analytics 1.0.1__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/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- 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 +4490 -378
- gitflow_analytics/cli_rich.py +503 -0
- 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 -398
- gitflow_analytics/core/analyzer.py +1320 -172
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +1554 -175
- gitflow_analytics/core/data_fetcher.py +1193 -0
- gitflow_analytics/core/identity.py +571 -185
- 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/base.py +13 -11
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +77 -59
- gitflow_analytics/extractors/tickets.py +841 -89
- 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 +258 -87
- gitflow_analytics/integrations/jira_integration.py +572 -123
- gitflow_analytics/integrations/orchestrator.py +206 -82
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +542 -179
- gitflow_analytics/models/database.py +986 -59
- 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 +29 -0
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
- 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 +438 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
- gitflow_analytics/qualitative/core/processor.py +673 -0
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +306 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
- gitflow_analytics/qualitative/utils/metrics.py +361 -0
- gitflow_analytics/qualitative/utils/text_processing.py +285 -0
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +550 -18
- 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 +1700 -216
- 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 +2289 -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 +5 -0
- gitflow_analytics/tui/app.py +724 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
- gitflow_analytics/tui/screens/configuration_screen.py +523 -0
- gitflow_analytics/tui/screens/loading_screen.py +348 -0
- gitflow_analytics/tui/screens/main_screen.py +321 -0
- gitflow_analytics/tui/screens/results_screen.py +722 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +255 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +187 -0
- gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Cost tracking and management for LLM API usage.
|
|
2
|
+
|
|
3
|
+
This module tracks API usage costs and provides warnings when
|
|
4
|
+
approaching or exceeding cost thresholds.
|
|
5
|
+
|
|
6
|
+
WHY: LLM API calls can be expensive. Tracking costs helps users
|
|
7
|
+
monitor expenses and make informed decisions about usage.
|
|
8
|
+
|
|
9
|
+
DESIGN DECISIONS:
|
|
10
|
+
- Support multiple pricing models for different providers
|
|
11
|
+
- Track costs at token level for accuracy
|
|
12
|
+
- Provide cost warnings and limits
|
|
13
|
+
- Support cost budgets and alerts
|
|
14
|
+
- Export cost data for analysis
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
from dataclasses import asdict, dataclass
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ModelPricing:
|
|
29
|
+
"""Pricing information for a specific model.
|
|
30
|
+
|
|
31
|
+
WHY: Different models have different pricing structures.
|
|
32
|
+
This allows accurate cost calculation per model.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
model_name: str
|
|
36
|
+
input_cost_per_million: float # Cost per 1M input tokens in USD
|
|
37
|
+
output_cost_per_million: float # Cost per 1M output tokens in USD
|
|
38
|
+
|
|
39
|
+
def calculate_cost(self, input_tokens: int, output_tokens: int) -> float:
|
|
40
|
+
"""Calculate cost for given token counts.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
input_tokens: Number of input/prompt tokens
|
|
44
|
+
output_tokens: Number of output/completion tokens
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Total cost in USD
|
|
48
|
+
"""
|
|
49
|
+
input_cost = (input_tokens / 1_000_000) * self.input_cost_per_million
|
|
50
|
+
output_cost = (output_tokens / 1_000_000) * self.output_cost_per_million
|
|
51
|
+
return input_cost + output_cost
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class CostRecord:
|
|
56
|
+
"""Record of a single API call's cost.
|
|
57
|
+
|
|
58
|
+
WHY: Detailed cost records enable analysis of spending patterns
|
|
59
|
+
and identification of optimization opportunities.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
timestamp: datetime
|
|
63
|
+
model: str
|
|
64
|
+
input_tokens: int
|
|
65
|
+
output_tokens: int
|
|
66
|
+
cost_usd: float
|
|
67
|
+
endpoint: str = "unknown"
|
|
68
|
+
batch_id: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CostTracker:
|
|
72
|
+
"""Tracks and manages LLM API usage costs.
|
|
73
|
+
|
|
74
|
+
WHY: Cost management is critical for production LLM usage.
|
|
75
|
+
This provides detailed tracking, warnings, and budgeting.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Default pricing for common models (as of 2024)
|
|
79
|
+
DEFAULT_PRICING = {
|
|
80
|
+
"gpt-4": ModelPricing("gpt-4", 30.0, 60.0),
|
|
81
|
+
"gpt-4-turbo": ModelPricing("gpt-4-turbo", 10.0, 30.0),
|
|
82
|
+
"gpt-4-turbo-preview": ModelPricing("gpt-4-turbo-preview", 10.0, 30.0),
|
|
83
|
+
"gpt-3.5-turbo": ModelPricing("gpt-3.5-turbo", 0.5, 1.5),
|
|
84
|
+
"gpt-3.5-turbo-16k": ModelPricing("gpt-3.5-turbo-16k", 1.0, 2.0),
|
|
85
|
+
"claude-3-opus": ModelPricing("claude-3-opus", 15.0, 75.0),
|
|
86
|
+
"claude-3-sonnet": ModelPricing("claude-3-sonnet", 3.0, 15.0),
|
|
87
|
+
"claude-3-haiku": ModelPricing("claude-3-haiku", 0.25, 1.25),
|
|
88
|
+
"claude-2.1": ModelPricing("claude-2.1", 8.0, 24.0),
|
|
89
|
+
"claude-2": ModelPricing("claude-2", 8.0, 24.0),
|
|
90
|
+
"mistral-7b": ModelPricing("mistral-7b", 0.25, 0.25),
|
|
91
|
+
"mistral-8x7b": ModelPricing("mistral-8x7b", 0.7, 0.7),
|
|
92
|
+
"llama-2-70b": ModelPricing("llama-2-70b", 0.7, 0.9),
|
|
93
|
+
"llama-2-13b": ModelPricing("llama-2-13b", 0.2, 0.25),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
cache_dir: Optional[Path] = None,
|
|
99
|
+
daily_budget: Optional[float] = None,
|
|
100
|
+
monthly_budget: Optional[float] = None,
|
|
101
|
+
):
|
|
102
|
+
"""Initialize cost tracker.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
cache_dir: Directory for storing cost records
|
|
106
|
+
daily_budget: Optional daily spending limit in USD
|
|
107
|
+
monthly_budget: Optional monthly spending limit in USD
|
|
108
|
+
"""
|
|
109
|
+
self.cache_dir = cache_dir or Path(".gitflow-cache")
|
|
110
|
+
self.cache_dir.mkdir(exist_ok=True)
|
|
111
|
+
|
|
112
|
+
self.daily_budget = daily_budget
|
|
113
|
+
self.monthly_budget = monthly_budget
|
|
114
|
+
|
|
115
|
+
# Current session costs
|
|
116
|
+
self.session_costs: list[CostRecord] = []
|
|
117
|
+
self.session_total = 0.0
|
|
118
|
+
|
|
119
|
+
# Current model pricing
|
|
120
|
+
self.current_pricing: Optional[ModelPricing] = None
|
|
121
|
+
|
|
122
|
+
# Load historical costs
|
|
123
|
+
self._load_cost_history()
|
|
124
|
+
|
|
125
|
+
def set_model_pricing(self, pricing: ModelPricing) -> None:
|
|
126
|
+
"""Set the pricing for the current model.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
pricing: Model pricing information
|
|
130
|
+
"""
|
|
131
|
+
self.current_pricing = pricing
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Set pricing for {pricing.model_name}: "
|
|
134
|
+
f"${pricing.input_cost_per_million}/1M input, "
|
|
135
|
+
f"${pricing.output_cost_per_million}/1M output"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def track_usage(
|
|
139
|
+
self,
|
|
140
|
+
input_tokens: int,
|
|
141
|
+
output_tokens: int,
|
|
142
|
+
model: Optional[str] = None,
|
|
143
|
+
batch_id: Optional[str] = None,
|
|
144
|
+
) -> float:
|
|
145
|
+
"""Track token usage and calculate cost.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
input_tokens: Number of input tokens used
|
|
149
|
+
output_tokens: Number of output tokens used
|
|
150
|
+
model: Optional model name override
|
|
151
|
+
batch_id: Optional batch identifier
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Cost of this usage in USD
|
|
155
|
+
"""
|
|
156
|
+
# Use current pricing or try to find from model name
|
|
157
|
+
pricing = self.current_pricing
|
|
158
|
+
if not pricing and model:
|
|
159
|
+
pricing = self._find_pricing_for_model(model)
|
|
160
|
+
if not pricing:
|
|
161
|
+
# Use a default conservative estimate
|
|
162
|
+
pricing = ModelPricing("unknown", 1.0, 1.0)
|
|
163
|
+
|
|
164
|
+
# Calculate cost
|
|
165
|
+
cost = pricing.calculate_cost(input_tokens, output_tokens)
|
|
166
|
+
|
|
167
|
+
# Create cost record
|
|
168
|
+
record = CostRecord(
|
|
169
|
+
timestamp=datetime.now(),
|
|
170
|
+
model=model or pricing.model_name,
|
|
171
|
+
input_tokens=input_tokens,
|
|
172
|
+
output_tokens=output_tokens,
|
|
173
|
+
cost_usd=cost,
|
|
174
|
+
batch_id=batch_id,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Track in session
|
|
178
|
+
self.session_costs.append(record)
|
|
179
|
+
self.session_total += cost
|
|
180
|
+
|
|
181
|
+
# Check budgets
|
|
182
|
+
self._check_budgets(cost)
|
|
183
|
+
|
|
184
|
+
# Log if significant cost
|
|
185
|
+
if cost > 0.01: # Log costs over 1 cent
|
|
186
|
+
logger.info(f"API call cost: ${cost:.4f} ({input_tokens} in, {output_tokens} out)")
|
|
187
|
+
|
|
188
|
+
return cost
|
|
189
|
+
|
|
190
|
+
def calculate_cost(
|
|
191
|
+
self, input_tokens: int, output_tokens: int, model: Optional[str] = None
|
|
192
|
+
) -> float:
|
|
193
|
+
"""Calculate cost without tracking (for estimates).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
input_tokens: Number of input tokens
|
|
197
|
+
output_tokens: Number of output tokens
|
|
198
|
+
model: Optional model name
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Estimated cost in USD
|
|
202
|
+
"""
|
|
203
|
+
pricing = self.current_pricing
|
|
204
|
+
if not pricing and model:
|
|
205
|
+
pricing = self._find_pricing_for_model(model)
|
|
206
|
+
if not pricing:
|
|
207
|
+
pricing = ModelPricing("unknown", 1.0, 1.0)
|
|
208
|
+
|
|
209
|
+
return pricing.calculate_cost(input_tokens, output_tokens)
|
|
210
|
+
|
|
211
|
+
def get_session_summary(self) -> dict:
|
|
212
|
+
"""Get summary of current session costs.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary with session cost information
|
|
216
|
+
"""
|
|
217
|
+
if not self.session_costs:
|
|
218
|
+
return {
|
|
219
|
+
"total_cost": 0.0,
|
|
220
|
+
"total_calls": 0,
|
|
221
|
+
"total_input_tokens": 0,
|
|
222
|
+
"total_output_tokens": 0,
|
|
223
|
+
"average_cost_per_call": 0.0,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
total_input = sum(r.input_tokens for r in self.session_costs)
|
|
227
|
+
total_output = sum(r.output_tokens for r in self.session_costs)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"total_cost": self.session_total,
|
|
231
|
+
"total_calls": len(self.session_costs),
|
|
232
|
+
"total_input_tokens": total_input,
|
|
233
|
+
"total_output_tokens": total_output,
|
|
234
|
+
"average_cost_per_call": self.session_total / len(self.session_costs),
|
|
235
|
+
"models_used": list(set(r.model for r in self.session_costs)),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
def get_daily_costs(self) -> float:
|
|
239
|
+
"""Get total costs for today.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Total cost in USD for the current day
|
|
243
|
+
"""
|
|
244
|
+
today = datetime.now().date()
|
|
245
|
+
daily_total = sum(r.cost_usd for r in self.session_costs if r.timestamp.date() == today)
|
|
246
|
+
|
|
247
|
+
# Also check historical costs
|
|
248
|
+
history_file = self._get_history_file()
|
|
249
|
+
if history_file.exists():
|
|
250
|
+
try:
|
|
251
|
+
with open(history_file) as f:
|
|
252
|
+
for line in f:
|
|
253
|
+
record_dict = json.loads(line)
|
|
254
|
+
timestamp = datetime.fromisoformat(record_dict["timestamp"])
|
|
255
|
+
if timestamp.date() == today:
|
|
256
|
+
daily_total += record_dict["cost_usd"]
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.warning(f"Error reading cost history: {e}")
|
|
259
|
+
|
|
260
|
+
return daily_total
|
|
261
|
+
|
|
262
|
+
def get_monthly_costs(self) -> float:
|
|
263
|
+
"""Get total costs for the current month.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Total cost in USD for the current month
|
|
267
|
+
"""
|
|
268
|
+
now = datetime.now()
|
|
269
|
+
month_start = datetime(now.year, now.month, 1)
|
|
270
|
+
|
|
271
|
+
monthly_total = sum(r.cost_usd for r in self.session_costs if r.timestamp >= month_start)
|
|
272
|
+
|
|
273
|
+
# Also check historical costs
|
|
274
|
+
history_file = self._get_history_file()
|
|
275
|
+
if history_file.exists():
|
|
276
|
+
try:
|
|
277
|
+
with open(history_file) as f:
|
|
278
|
+
for line in f:
|
|
279
|
+
record_dict = json.loads(line)
|
|
280
|
+
timestamp = datetime.fromisoformat(record_dict["timestamp"])
|
|
281
|
+
if timestamp >= month_start:
|
|
282
|
+
monthly_total += record_dict["cost_usd"]
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.warning(f"Error reading cost history: {e}")
|
|
285
|
+
|
|
286
|
+
return monthly_total
|
|
287
|
+
|
|
288
|
+
def save_session(self) -> None:
|
|
289
|
+
"""Save current session costs to history file.
|
|
290
|
+
|
|
291
|
+
WHY: Persisting cost data enables long-term tracking
|
|
292
|
+
and analysis of LLM usage patterns.
|
|
293
|
+
"""
|
|
294
|
+
if not self.session_costs:
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
history_file = self._get_history_file()
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
with open(history_file, "a") as f:
|
|
301
|
+
for record in self.session_costs:
|
|
302
|
+
# Convert to dict and handle datetime
|
|
303
|
+
record_dict = asdict(record)
|
|
304
|
+
record_dict["timestamp"] = record.timestamp.isoformat()
|
|
305
|
+
f.write(json.dumps(record_dict) + "\n")
|
|
306
|
+
|
|
307
|
+
logger.info(f"Saved {len(self.session_costs)} cost records to history")
|
|
308
|
+
|
|
309
|
+
# Clear session costs after saving
|
|
310
|
+
self.session_costs = []
|
|
311
|
+
self.session_total = 0.0
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"Failed to save cost history: {e}")
|
|
315
|
+
|
|
316
|
+
def export_costs(self, output_file: Path) -> None:
|
|
317
|
+
"""Export all cost data to a JSON file.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
output_file: Path to export file
|
|
321
|
+
"""
|
|
322
|
+
all_records = []
|
|
323
|
+
|
|
324
|
+
# Add current session
|
|
325
|
+
for record in self.session_costs:
|
|
326
|
+
record_dict = asdict(record)
|
|
327
|
+
record_dict["timestamp"] = record.timestamp.isoformat()
|
|
328
|
+
all_records.append(record_dict)
|
|
329
|
+
|
|
330
|
+
# Add historical
|
|
331
|
+
history_file = self._get_history_file()
|
|
332
|
+
if history_file.exists():
|
|
333
|
+
try:
|
|
334
|
+
with open(history_file) as f:
|
|
335
|
+
for line in f:
|
|
336
|
+
all_records.append(json.loads(line))
|
|
337
|
+
except Exception as e:
|
|
338
|
+
logger.warning(f"Error reading cost history: {e}")
|
|
339
|
+
|
|
340
|
+
# Write export file
|
|
341
|
+
with open(output_file, "w") as f:
|
|
342
|
+
json.dump(
|
|
343
|
+
{
|
|
344
|
+
"records": all_records,
|
|
345
|
+
"summary": self.get_session_summary(),
|
|
346
|
+
"daily_total": self.get_daily_costs(),
|
|
347
|
+
"monthly_total": self.get_monthly_costs(),
|
|
348
|
+
},
|
|
349
|
+
f,
|
|
350
|
+
indent=2,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
logger.info(f"Exported {len(all_records)} cost records to {output_file}")
|
|
354
|
+
|
|
355
|
+
def _find_pricing_for_model(self, model: str) -> Optional[ModelPricing]:
|
|
356
|
+
"""Find pricing information for a model name.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
model: Model name to find pricing for
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
ModelPricing or None if not found
|
|
363
|
+
"""
|
|
364
|
+
model_lower = model.lower()
|
|
365
|
+
|
|
366
|
+
# Check exact matches first
|
|
367
|
+
if model_lower in self.DEFAULT_PRICING:
|
|
368
|
+
return self.DEFAULT_PRICING[model_lower]
|
|
369
|
+
|
|
370
|
+
# Check partial matches
|
|
371
|
+
for key, pricing in self.DEFAULT_PRICING.items():
|
|
372
|
+
if key in model_lower or model_lower in key:
|
|
373
|
+
return pricing
|
|
374
|
+
|
|
375
|
+
# Check for common prefixes
|
|
376
|
+
if "gpt-4" in model_lower:
|
|
377
|
+
return self.DEFAULT_PRICING.get("gpt-4-turbo", self.DEFAULT_PRICING["gpt-4"])
|
|
378
|
+
elif "gpt-3" in model_lower:
|
|
379
|
+
return self.DEFAULT_PRICING["gpt-3.5-turbo"]
|
|
380
|
+
elif "claude" in model_lower:
|
|
381
|
+
return self.DEFAULT_PRICING.get("claude-2", ModelPricing("claude", 8.0, 24.0))
|
|
382
|
+
elif "mistral" in model_lower:
|
|
383
|
+
return self.DEFAULT_PRICING.get("mistral-7b", ModelPricing("mistral", 0.25, 0.25))
|
|
384
|
+
elif "llama" in model_lower:
|
|
385
|
+
return self.DEFAULT_PRICING.get("llama-2-70b", ModelPricing("llama", 0.7, 0.9))
|
|
386
|
+
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
def _check_budgets(self, new_cost: float) -> None:
|
|
390
|
+
"""Check if budgets are being exceeded.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
new_cost: Cost of the latest API call
|
|
394
|
+
"""
|
|
395
|
+
# Check daily budget
|
|
396
|
+
if self.daily_budget:
|
|
397
|
+
daily_total = self.get_daily_costs()
|
|
398
|
+
if daily_total > self.daily_budget:
|
|
399
|
+
logger.warning(
|
|
400
|
+
f"DAILY BUDGET EXCEEDED: ${daily_total:.2f} > ${self.daily_budget:.2f}"
|
|
401
|
+
)
|
|
402
|
+
elif daily_total > self.daily_budget * 0.8:
|
|
403
|
+
logger.warning(
|
|
404
|
+
f"Approaching daily budget: ${daily_total:.2f} of ${self.daily_budget:.2f}"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# Check monthly budget
|
|
408
|
+
if self.monthly_budget:
|
|
409
|
+
monthly_total = self.get_monthly_costs()
|
|
410
|
+
if monthly_total > self.monthly_budget:
|
|
411
|
+
logger.warning(
|
|
412
|
+
f"MONTHLY BUDGET EXCEEDED: ${monthly_total:.2f} > ${self.monthly_budget:.2f}"
|
|
413
|
+
)
|
|
414
|
+
elif monthly_total > self.monthly_budget * 0.8:
|
|
415
|
+
logger.warning(
|
|
416
|
+
f"Approaching monthly budget: ${monthly_total:.2f} of ${self.monthly_budget:.2f}"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def _get_history_file(self) -> Path:
|
|
420
|
+
"""Get path to cost history file.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Path to history file
|
|
424
|
+
"""
|
|
425
|
+
return self.cache_dir / "llm_costs.jsonl"
|
|
426
|
+
|
|
427
|
+
def _load_cost_history(self) -> None:
|
|
428
|
+
"""Load cost history from file.
|
|
429
|
+
|
|
430
|
+
WHY: Loading historical costs enables budget tracking
|
|
431
|
+
across multiple sessions.
|
|
432
|
+
"""
|
|
433
|
+
# For now, we don't load into memory to avoid memory issues
|
|
434
|
+
# History is queried when needed for daily/monthly totals
|
|
435
|
+
pass
|