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.
Files changed (119) hide show
  1. gitflow_analytics/__init__.py +11 -11
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/classification/__init__.py +31 -0
  4. gitflow_analytics/classification/batch_classifier.py +752 -0
  5. gitflow_analytics/classification/classifier.py +464 -0
  6. gitflow_analytics/classification/feature_extractor.py +725 -0
  7. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  8. gitflow_analytics/classification/model.py +455 -0
  9. gitflow_analytics/cli.py +4490 -378
  10. gitflow_analytics/cli_rich.py +503 -0
  11. gitflow_analytics/config/__init__.py +43 -0
  12. gitflow_analytics/config/errors.py +261 -0
  13. gitflow_analytics/config/loader.py +904 -0
  14. gitflow_analytics/config/profiles.py +264 -0
  15. gitflow_analytics/config/repository.py +124 -0
  16. gitflow_analytics/config/schema.py +441 -0
  17. gitflow_analytics/config/validator.py +154 -0
  18. gitflow_analytics/config.py +44 -398
  19. gitflow_analytics/core/analyzer.py +1320 -172
  20. gitflow_analytics/core/branch_mapper.py +132 -132
  21. gitflow_analytics/core/cache.py +1554 -175
  22. gitflow_analytics/core/data_fetcher.py +1193 -0
  23. gitflow_analytics/core/identity.py +571 -185
  24. gitflow_analytics/core/metrics_storage.py +526 -0
  25. gitflow_analytics/core/progress.py +372 -0
  26. gitflow_analytics/core/schema_version.py +269 -0
  27. gitflow_analytics/extractors/base.py +13 -11
  28. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  29. gitflow_analytics/extractors/story_points.py +77 -59
  30. gitflow_analytics/extractors/tickets.py +841 -89
  31. gitflow_analytics/identity_llm/__init__.py +6 -0
  32. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  33. gitflow_analytics/identity_llm/analyzer.py +464 -0
  34. gitflow_analytics/identity_llm/models.py +76 -0
  35. gitflow_analytics/integrations/github_integration.py +258 -87
  36. gitflow_analytics/integrations/jira_integration.py +572 -123
  37. gitflow_analytics/integrations/orchestrator.py +206 -82
  38. gitflow_analytics/metrics/activity_scoring.py +322 -0
  39. gitflow_analytics/metrics/branch_health.py +470 -0
  40. gitflow_analytics/metrics/dora.py +542 -179
  41. gitflow_analytics/models/database.py +986 -59
  42. gitflow_analytics/pm_framework/__init__.py +115 -0
  43. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  44. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  45. gitflow_analytics/pm_framework/base.py +406 -0
  46. gitflow_analytics/pm_framework/models.py +211 -0
  47. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  48. gitflow_analytics/pm_framework/registry.py +333 -0
  49. gitflow_analytics/qualitative/__init__.py +29 -0
  50. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  51. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  52. gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
  53. gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
  54. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
  55. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  56. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  57. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  58. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  59. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  60. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  61. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  62. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  63. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  64. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
  65. gitflow_analytics/qualitative/core/__init__.py +13 -0
  66. gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
  67. gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
  68. gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
  69. gitflow_analytics/qualitative/core/processor.py +673 -0
  70. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  71. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  72. gitflow_analytics/qualitative/models/__init__.py +25 -0
  73. gitflow_analytics/qualitative/models/schemas.py +306 -0
  74. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  75. gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
  76. gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
  77. gitflow_analytics/qualitative/utils/metrics.py +361 -0
  78. gitflow_analytics/qualitative/utils/text_processing.py +285 -0
  79. gitflow_analytics/reports/__init__.py +100 -0
  80. gitflow_analytics/reports/analytics_writer.py +550 -18
  81. gitflow_analytics/reports/base.py +648 -0
  82. gitflow_analytics/reports/branch_health_writer.py +322 -0
  83. gitflow_analytics/reports/classification_writer.py +924 -0
  84. gitflow_analytics/reports/cli_integration.py +427 -0
  85. gitflow_analytics/reports/csv_writer.py +1700 -216
  86. gitflow_analytics/reports/data_models.py +504 -0
  87. gitflow_analytics/reports/database_report_generator.py +427 -0
  88. gitflow_analytics/reports/example_usage.py +344 -0
  89. gitflow_analytics/reports/factory.py +499 -0
  90. gitflow_analytics/reports/formatters.py +698 -0
  91. gitflow_analytics/reports/html_generator.py +1116 -0
  92. gitflow_analytics/reports/interfaces.py +489 -0
  93. gitflow_analytics/reports/json_exporter.py +2770 -0
  94. gitflow_analytics/reports/narrative_writer.py +2289 -158
  95. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  96. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  97. gitflow_analytics/training/__init__.py +5 -0
  98. gitflow_analytics/training/model_loader.py +377 -0
  99. gitflow_analytics/training/pipeline.py +550 -0
  100. gitflow_analytics/tui/__init__.py +5 -0
  101. gitflow_analytics/tui/app.py +724 -0
  102. gitflow_analytics/tui/screens/__init__.py +8 -0
  103. gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
  104. gitflow_analytics/tui/screens/configuration_screen.py +523 -0
  105. gitflow_analytics/tui/screens/loading_screen.py +348 -0
  106. gitflow_analytics/tui/screens/main_screen.py +321 -0
  107. gitflow_analytics/tui/screens/results_screen.py +722 -0
  108. gitflow_analytics/tui/widgets/__init__.py +7 -0
  109. gitflow_analytics/tui/widgets/data_table.py +255 -0
  110. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  111. gitflow_analytics/tui/widgets/progress_widget.py +187 -0
  112. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  113. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  114. gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
  115. gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
  116. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  117. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  118. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  119. {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