gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.11__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.
Files changed (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4158 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +905 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +444 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1285 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.11.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.11.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.11.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 json
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
- '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},
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
- 'openai/gpt-3.5-turbo': {'input': 0.5, 'output': 1.5},
43
- 'openai/gpt-4': {'input': 30.0, 'output': 60.0},
44
- 'openai/gpt-4-turbo': {'input': 10.0, 'output': 30.0},
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
- 'meta-llama/llama-3.1-8b-instruct:free': {'input': 0.0, 'output': 0.0},
48
- 'meta-llama/llama-3.1-70b-instruct:free': {'input': 0.0, 'output': 0.0},
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
- 'google/gemini-pro': {'input': 0.5, 'output': 1.5},
52
- 'mistralai/mixtral-8x7b-instruct': {'input': 0.27, 'output': 0.27},
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: List[LLMCall] = []
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(self, model: str, input_tokens: int, output_tokens: int,
74
- processing_time: float, batch_size: int = 1,
75
- success: bool = True, error_message: Optional[str] = None) -> float:
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 for call in self.calls
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) -> Dict[str, any]:
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
- 'total_calls': 0,
176
- 'total_cost': 0.0,
177
- 'total_tokens': 0,
178
- 'avg_cost_per_call': 0.0,
179
- 'model_usage': {},
180
- 'success_rate': 1.0
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] = {'calls': 0, 'cost': 0.0, 'tokens': 0}
194
- model_usage[call.model]['calls'] += 1
195
- model_usage[call.model]['cost'] += call.estimated_cost
196
- model_usage[call.model]['tokens'] += call.input_tokens + call.output_tokens
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
- 'total_calls': len(recent_calls),
200
- 'successful_calls': len(successful_calls),
201
- 'total_cost': total_cost,
202
- 'total_tokens': total_tokens,
203
- 'avg_cost_per_call': total_cost / len(successful_calls) if successful_calls else 0.0,
204
- 'model_usage': model_usage,
205
- 'success_rate': len(successful_calls) / len(recent_calls) if recent_calls else 1.0,
206
- 'daily_average_cost': total_cost / days,
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) -> List[str]:
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['total_calls'] == 0:
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['model_usage']
223
- total_cost = stats['total_cost']
224
-
225
- expensive_models = ['anthropic/claude-3-opus', 'openai/gpt-4']
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('cost', 0)
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('meta-llama/llama-3.1-8b-instruct:free', {}).get('calls', 0)
238
- if free_usage < stats['total_calls'] * 0.5:
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
- call.batch_size for call in self.calls[-50:] # Last 50 calls
252
- ) / min(50, len(self.calls))
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
- "Increase batch size for LLM calls to improve cost efficiency"
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['input']
280
- output_price = pricing['output']
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, 'r') as f:
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('calls', []):
300
+ for call_data in data.get("calls", []):
299
301
  call = LLMCall(
300
- timestamp=datetime.fromisoformat(call_data['timestamp']),
301
- model=call_data['model'],
302
- input_tokens=call_data['input_tokens'],
303
- output_tokens=call_data['output_tokens'],
304
- processing_time_ms=call_data['processing_time_ms'],
305
- estimated_cost=call_data['estimated_cost'],
306
- batch_size=call_data.get('batch_size', 1),
307
- success=call_data.get('success', True),
308
- error_message=call_data.get('error_message')
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
- 'calls': [
325
+ "calls": [
324
326
  {
325
- 'timestamp': call.timestamp.isoformat(),
326
- 'model': call.model,
327
- 'input_tokens': call.input_tokens,
328
- 'output_tokens': call.output_tokens,
329
- 'processing_time_ms': call.processing_time_ms,
330
- 'estimated_cost': call.estimated_cost,
331
- 'batch_size': call.batch_size,
332
- 'success': call.success,
333
- 'error_message': call.error_message
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, 'w') as f:
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}")