local-deep-research 0.4.4__py3-none-any.whl → 0.5.2__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 (220) hide show
  1. local_deep_research/__init__.py +7 -0
  2. local_deep_research/__version__.py +1 -1
  3. local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
  4. local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
  5. local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
  6. local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
  7. local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
  8. local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
  9. local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
  10. local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
  11. local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
  12. local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
  13. local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
  14. local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
  15. local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
  16. local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
  17. local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
  18. local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
  19. local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
  20. local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
  21. local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
  22. local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
  23. local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
  24. local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
  25. local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
  26. local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
  27. local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
  28. local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
  29. local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
  30. local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
  31. local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
  32. local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
  33. local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
  34. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
  35. local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
  36. local_deep_research/advanced_search_system/findings/repository.py +54 -17
  37. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
  38. local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
  39. local_deep_research/advanced_search_system/questions/__init__.py +16 -0
  40. local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
  41. local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
  42. local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
  43. local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
  44. local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
  45. local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
  46. local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
  47. local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
  48. local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
  49. local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
  50. local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
  51. local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
  52. local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
  53. local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
  54. local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
  55. local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
  56. local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
  57. local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
  58. local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
  59. local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
  60. local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
  61. local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
  62. local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
  63. local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
  64. local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
  65. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
  66. local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
  67. local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
  68. local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
  69. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
  70. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
  71. local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
  72. local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
  73. local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
  74. local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
  75. local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
  76. local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
  77. local_deep_research/api/benchmark_functions.py +6 -2
  78. local_deep_research/api/research_functions.py +10 -4
  79. local_deep_research/benchmarks/__init__.py +9 -7
  80. local_deep_research/benchmarks/benchmark_functions.py +6 -2
  81. local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
  82. local_deep_research/benchmarks/cli.py +38 -13
  83. local_deep_research/benchmarks/comparison/__init__.py +4 -2
  84. local_deep_research/benchmarks/comparison/evaluator.py +316 -239
  85. local_deep_research/benchmarks/datasets/__init__.py +1 -1
  86. local_deep_research/benchmarks/datasets/base.py +91 -72
  87. local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
  88. local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
  89. local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
  90. local_deep_research/benchmarks/datasets/utils.py +48 -29
  91. local_deep_research/benchmarks/datasets.py +4 -11
  92. local_deep_research/benchmarks/efficiency/__init__.py +8 -4
  93. local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
  94. local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
  95. local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
  96. local_deep_research/benchmarks/evaluators/composite.py +6 -2
  97. local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
  98. local_deep_research/benchmarks/graders.py +32 -10
  99. local_deep_research/benchmarks/metrics/README.md +1 -1
  100. local_deep_research/benchmarks/metrics/calculation.py +25 -10
  101. local_deep_research/benchmarks/metrics/reporting.py +7 -3
  102. local_deep_research/benchmarks/metrics/visualization.py +42 -23
  103. local_deep_research/benchmarks/metrics.py +1 -1
  104. local_deep_research/benchmarks/optimization/__init__.py +3 -1
  105. local_deep_research/benchmarks/optimization/api.py +7 -1
  106. local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
  107. local_deep_research/benchmarks/runners.py +48 -15
  108. local_deep_research/citation_handler.py +65 -92
  109. local_deep_research/citation_handlers/__init__.py +15 -0
  110. local_deep_research/citation_handlers/base_citation_handler.py +70 -0
  111. local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
  112. local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
  113. local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
  114. local_deep_research/config/llm_config.py +271 -169
  115. local_deep_research/config/search_config.py +14 -5
  116. local_deep_research/defaults/__init__.py +0 -1
  117. local_deep_research/metrics/__init__.py +13 -0
  118. local_deep_research/metrics/database.py +58 -0
  119. local_deep_research/metrics/db_models.py +115 -0
  120. local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
  121. local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
  122. local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
  123. local_deep_research/metrics/migrate_research_ratings.py +31 -0
  124. local_deep_research/metrics/models.py +61 -0
  125. local_deep_research/metrics/pricing/__init__.py +12 -0
  126. local_deep_research/metrics/pricing/cost_calculator.py +237 -0
  127. local_deep_research/metrics/pricing/pricing_cache.py +143 -0
  128. local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
  129. local_deep_research/metrics/query_utils.py +51 -0
  130. local_deep_research/metrics/search_tracker.py +380 -0
  131. local_deep_research/metrics/token_counter.py +1078 -0
  132. local_deep_research/migrate_db.py +3 -1
  133. local_deep_research/report_generator.py +22 -8
  134. local_deep_research/search_system.py +390 -9
  135. local_deep_research/test_migration.py +15 -5
  136. local_deep_research/utilities/db_utils.py +7 -4
  137. local_deep_research/utilities/es_utils.py +115 -104
  138. local_deep_research/utilities/llm_utils.py +15 -5
  139. local_deep_research/utilities/log_utils.py +151 -0
  140. local_deep_research/utilities/search_cache.py +387 -0
  141. local_deep_research/utilities/search_utilities.py +14 -6
  142. local_deep_research/utilities/threading_utils.py +92 -0
  143. local_deep_research/utilities/url_utils.py +6 -0
  144. local_deep_research/web/api.py +347 -0
  145. local_deep_research/web/app.py +13 -17
  146. local_deep_research/web/app_factory.py +71 -66
  147. local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
  148. local_deep_research/web/database/migrations.py +20 -3
  149. local_deep_research/web/database/models.py +74 -25
  150. local_deep_research/web/database/schema_upgrade.py +49 -29
  151. local_deep_research/web/models/database.py +63 -83
  152. local_deep_research/web/routes/api_routes.py +56 -22
  153. local_deep_research/web/routes/benchmark_routes.py +4 -1
  154. local_deep_research/web/routes/globals.py +22 -0
  155. local_deep_research/web/routes/history_routes.py +71 -46
  156. local_deep_research/web/routes/metrics_routes.py +1155 -0
  157. local_deep_research/web/routes/research_routes.py +192 -54
  158. local_deep_research/web/routes/settings_routes.py +156 -55
  159. local_deep_research/web/services/research_service.py +412 -251
  160. local_deep_research/web/services/resource_service.py +36 -11
  161. local_deep_research/web/services/settings_manager.py +55 -17
  162. local_deep_research/web/services/settings_service.py +12 -4
  163. local_deep_research/web/services/socket_service.py +295 -188
  164. local_deep_research/web/static/css/custom_dropdown.css +180 -0
  165. local_deep_research/web/static/css/styles.css +39 -1
  166. local_deep_research/web/static/js/components/detail.js +633 -267
  167. local_deep_research/web/static/js/components/details.js +751 -0
  168. local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
  169. local_deep_research/web/static/js/components/fallback/ui.js +23 -23
  170. local_deep_research/web/static/js/components/history.js +76 -76
  171. local_deep_research/web/static/js/components/logpanel.js +61 -13
  172. local_deep_research/web/static/js/components/progress.js +13 -2
  173. local_deep_research/web/static/js/components/research.js +99 -12
  174. local_deep_research/web/static/js/components/results.js +239 -106
  175. local_deep_research/web/static/js/main.js +40 -40
  176. local_deep_research/web/static/js/services/audio.js +1 -1
  177. local_deep_research/web/static/js/services/formatting.js +11 -11
  178. local_deep_research/web/static/js/services/keyboard.js +157 -0
  179. local_deep_research/web/static/js/services/pdf.js +80 -80
  180. local_deep_research/web/static/sounds/README.md +1 -1
  181. local_deep_research/web/templates/base.html +1 -0
  182. local_deep_research/web/templates/components/log_panel.html +7 -1
  183. local_deep_research/web/templates/components/mobile_nav.html +1 -1
  184. local_deep_research/web/templates/components/sidebar.html +3 -0
  185. local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
  186. local_deep_research/web/templates/pages/details.html +325 -24
  187. local_deep_research/web/templates/pages/history.html +1 -1
  188. local_deep_research/web/templates/pages/metrics.html +1929 -0
  189. local_deep_research/web/templates/pages/progress.html +2 -2
  190. local_deep_research/web/templates/pages/research.html +53 -17
  191. local_deep_research/web/templates/pages/results.html +12 -1
  192. local_deep_research/web/templates/pages/star_reviews.html +803 -0
  193. local_deep_research/web/utils/formatters.py +9 -3
  194. local_deep_research/web_search_engines/default_search_engines.py +5 -3
  195. local_deep_research/web_search_engines/engines/full_search.py +8 -2
  196. local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
  197. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
  198. local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
  199. local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
  200. local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
  201. local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
  202. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
  203. local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
  204. local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
  205. local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
  206. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
  207. local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
  208. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
  209. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
  210. local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
  211. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
  212. local_deep_research/web_search_engines/search_engine_base.py +83 -35
  213. local_deep_research/web_search_engines/search_engine_factory.py +25 -8
  214. local_deep_research/web_search_engines/search_engines_config.py +9 -3
  215. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/METADATA +7 -1
  216. local_deep_research-0.5.2.dist-info/RECORD +265 -0
  217. local_deep_research-0.4.4.dist-info/RECORD +0 -177
  218. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/WHEEL +0 -0
  219. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/entry_points.txt +0 -0
  220. {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,237 @@
1
+ """
2
+ Cost Calculator
3
+
4
+ Calculates LLM usage costs based on token usage and pricing data.
5
+ Integrates with pricing fetcher and cache systems.
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from .pricing_cache import PricingCache
12
+ from .pricing_fetcher import PricingFetcher
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CostCalculator:
18
+ """Calculates LLM usage costs."""
19
+
20
+ def __init__(self, cache_dir: Optional[str] = None):
21
+ self.cache = PricingCache(cache_dir)
22
+ self.pricing_fetcher = None
23
+
24
+ async def __aenter__(self):
25
+ self.pricing_fetcher = PricingFetcher()
26
+ await self.pricing_fetcher.__aenter__()
27
+ return self
28
+
29
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
30
+ if self.pricing_fetcher:
31
+ await self.pricing_fetcher.__aexit__(exc_type, exc_val, exc_tb)
32
+
33
+ async def get_model_pricing(
34
+ self, model_name: str, provider: str = None
35
+ ) -> Dict[str, float]:
36
+ """Get pricing for a model and provider (cached or fetched)."""
37
+ # Create cache key that includes provider
38
+ cache_key = f"{provider}:{model_name}" if provider else model_name
39
+
40
+ # Try cache first
41
+ cached_pricing = self.cache.get(f"model:{cache_key}")
42
+ if cached_pricing:
43
+ return cached_pricing
44
+
45
+ # Fetch from API
46
+ if self.pricing_fetcher:
47
+ pricing = await self.pricing_fetcher.get_model_pricing(
48
+ model_name, provider
49
+ )
50
+ if pricing:
51
+ self.cache.set(f"model:{cache_key}", pricing)
52
+ return pricing
53
+
54
+ # No pricing found
55
+ logger.warning(
56
+ f"No pricing found for {model_name} (provider: {provider})"
57
+ )
58
+ return None
59
+
60
+ async def calculate_cost(
61
+ self,
62
+ model_name: str,
63
+ prompt_tokens: int,
64
+ completion_tokens: int,
65
+ provider: str = None,
66
+ ) -> Dict[str, float]:
67
+ """
68
+ Calculate cost for a single LLM call.
69
+
70
+ Returns:
71
+ Dict with prompt_cost, completion_cost, total_cost
72
+ """
73
+ pricing = await self.get_model_pricing(model_name, provider)
74
+
75
+ # If no pricing found, return zero cost
76
+ if pricing is None:
77
+ return {
78
+ "prompt_cost": 0.0,
79
+ "completion_cost": 0.0,
80
+ "total_cost": 0.0,
81
+ "pricing_used": None,
82
+ "error": "No pricing data available for this model",
83
+ }
84
+
85
+ # Convert tokens to thousands for pricing calculation
86
+ prompt_cost = (prompt_tokens / 1000) * pricing["prompt"]
87
+ completion_cost = (completion_tokens / 1000) * pricing["completion"]
88
+ total_cost = prompt_cost + completion_cost
89
+
90
+ return {
91
+ "prompt_cost": round(prompt_cost, 6),
92
+ "completion_cost": round(completion_cost, 6),
93
+ "total_cost": round(total_cost, 6),
94
+ "pricing_used": pricing,
95
+ }
96
+
97
+ async def calculate_batch_costs(
98
+ self, usage_records: List[Dict[str, Any]]
99
+ ) -> List[Dict[str, Any]]:
100
+ """
101
+ Calculate costs for multiple usage records.
102
+
103
+ Expected record format:
104
+ {
105
+ "model_name": str,
106
+ "provider": str (optional),
107
+ "prompt_tokens": int,
108
+ "completion_tokens": int,
109
+ "research_id": int (optional),
110
+ "timestamp": datetime (optional)
111
+ }
112
+ """
113
+ results = []
114
+
115
+ for record in usage_records:
116
+ try:
117
+ cost_data = await self.calculate_cost(
118
+ record["model_name"],
119
+ record["prompt_tokens"],
120
+ record["completion_tokens"],
121
+ record.get("provider"),
122
+ )
123
+
124
+ result = {**record, **cost_data}
125
+ results.append(result)
126
+
127
+ except Exception as e:
128
+ logger.error(
129
+ f"Failed to calculate cost for record {record}: {e}"
130
+ )
131
+ # Add record with zero cost on error
132
+ results.append(
133
+ {
134
+ **record,
135
+ "prompt_cost": 0.0,
136
+ "completion_cost": 0.0,
137
+ "total_cost": 0.0,
138
+ "error": str(e),
139
+ }
140
+ )
141
+
142
+ return results
143
+
144
+ def calculate_cost_sync(
145
+ self, model_name: str, prompt_tokens: int, completion_tokens: int
146
+ ) -> Dict[str, float]:
147
+ """
148
+ Synchronous cost calculation using cached pricing only.
149
+ Fallback for when async is not available.
150
+ """
151
+ # Use cached pricing only
152
+ pricing = self.cache.get_model_pricing(model_name)
153
+ if not pricing:
154
+ # Use static fallback with exact matching only
155
+ fetcher = PricingFetcher()
156
+ # Try exact match
157
+ pricing = fetcher.static_pricing.get(model_name)
158
+ if not pricing:
159
+ # Try exact match without provider prefix
160
+ if "/" in model_name:
161
+ model_only = model_name.split("/")[-1]
162
+ pricing = fetcher.static_pricing.get(model_only)
163
+
164
+ # If no pricing found, return zero cost
165
+ if not pricing:
166
+ return {
167
+ "prompt_cost": 0.0,
168
+ "completion_cost": 0.0,
169
+ "total_cost": 0.0,
170
+ "pricing_used": None,
171
+ "error": "No pricing data available for this model",
172
+ }
173
+
174
+ prompt_cost = (prompt_tokens / 1000) * pricing["prompt"]
175
+ completion_cost = (completion_tokens / 1000) * pricing["completion"]
176
+ total_cost = prompt_cost + completion_cost
177
+
178
+ return {
179
+ "prompt_cost": round(prompt_cost, 6),
180
+ "completion_cost": round(completion_cost, 6),
181
+ "total_cost": round(total_cost, 6),
182
+ "pricing_used": pricing,
183
+ }
184
+
185
+ async def get_research_cost_summary(
186
+ self, usage_records: List[Dict[str, Any]]
187
+ ) -> Dict[str, Any]:
188
+ """
189
+ Get cost summary for research session(s).
190
+ """
191
+ costs = await self.calculate_batch_costs(usage_records)
192
+
193
+ total_cost = sum(c["total_cost"] for c in costs)
194
+ total_prompt_cost = sum(c["prompt_cost"] for c in costs)
195
+ total_completion_cost = sum(c["completion_cost"] for c in costs)
196
+
197
+ total_prompt_tokens = sum(r["prompt_tokens"] for r in usage_records)
198
+ total_completion_tokens = sum(
199
+ r["completion_tokens"] for r in usage_records
200
+ )
201
+ total_tokens = total_prompt_tokens + total_completion_tokens
202
+
203
+ # Model breakdown
204
+ model_costs = {}
205
+ for cost in costs:
206
+ model = cost["model_name"]
207
+ if model not in model_costs:
208
+ model_costs[model] = {
209
+ "total_cost": 0.0,
210
+ "prompt_tokens": 0,
211
+ "completion_tokens": 0,
212
+ "calls": 0,
213
+ }
214
+
215
+ model_costs[model]["total_cost"] += cost["total_cost"]
216
+ model_costs[model]["prompt_tokens"] += cost["prompt_tokens"]
217
+ model_costs[model]["completion_tokens"] += cost["completion_tokens"]
218
+ model_costs[model]["calls"] += 1
219
+
220
+ return {
221
+ "total_cost": round(total_cost, 6),
222
+ "prompt_cost": round(total_prompt_cost, 6),
223
+ "completion_cost": round(total_completion_cost, 6),
224
+ "total_tokens": total_tokens,
225
+ "prompt_tokens": total_prompt_tokens,
226
+ "completion_tokens": total_completion_tokens,
227
+ "total_calls": len(usage_records),
228
+ "model_breakdown": model_costs,
229
+ "avg_cost_per_call": (
230
+ round(total_cost / len(usage_records), 6)
231
+ if usage_records
232
+ else 0.0
233
+ ),
234
+ "cost_per_token": (
235
+ round(total_cost / total_tokens, 8) if total_tokens > 0 else 0.0
236
+ ),
237
+ }
@@ -0,0 +1,143 @@
1
+ """
2
+ Pricing Cache System
3
+
4
+ Caches pricing data to avoid repeated API calls and improve performance.
5
+ Includes cache expiration and refresh mechanisms.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import time
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+ from typing import Any, Dict, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class PricingCache:
19
+ """Cache for LLM pricing data."""
20
+
21
+ def __init__(self, cache_dir: Optional[str] = None, cache_ttl: int = 3600):
22
+ """
23
+ Initialize pricing cache.
24
+
25
+ Args:
26
+ cache_dir: Directory to store cache files
27
+ cache_ttl: Cache time-to-live in seconds (default: 1 hour)
28
+ """
29
+ self.cache_ttl = cache_ttl
30
+
31
+ if cache_dir:
32
+ self.cache_dir = Path(cache_dir)
33
+ else:
34
+ # Default to data directory
35
+ self.cache_dir = Path.cwd() / "data" / "cache" / "pricing"
36
+
37
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
38
+ self.cache_file = self.cache_dir / "pricing_cache.json"
39
+
40
+ self._cache = {}
41
+ self._load_cache()
42
+
43
+ def _load_cache(self):
44
+ """Load cache from disk."""
45
+ try:
46
+ if self.cache_file.exists():
47
+ with open(self.cache_file, "r") as f:
48
+ data = json.load(f)
49
+ self._cache = data.get("cache", {})
50
+ logger.info(
51
+ f"Loaded pricing cache with {len(self._cache)} entries"
52
+ )
53
+ except Exception as e:
54
+ logger.warning(f"Failed to load pricing cache: {e}")
55
+ self._cache = {}
56
+
57
+ def _save_cache(self):
58
+ """Save cache to disk."""
59
+ try:
60
+ cache_data = {
61
+ "cache": self._cache,
62
+ "last_updated": datetime.now().isoformat(),
63
+ }
64
+ with open(self.cache_file, "w") as f:
65
+ json.dump(cache_data, f, indent=2)
66
+ except Exception as e:
67
+ logger.warning(f"Failed to save pricing cache: {e}")
68
+
69
+ def _is_expired(self, timestamp: float) -> bool:
70
+ """Check if cache entry is expired."""
71
+ return (time.time() - timestamp) > self.cache_ttl
72
+
73
+ def get(self, key: str) -> Optional[Any]:
74
+ """Get cached pricing data."""
75
+ if key not in self._cache:
76
+ return None
77
+
78
+ entry = self._cache[key]
79
+ if self._is_expired(entry["timestamp"]):
80
+ # Remove expired entry
81
+ del self._cache[key]
82
+ self._save_cache()
83
+ return None
84
+
85
+ return entry["data"]
86
+
87
+ def set(self, key: str, data: Any):
88
+ """Set cached pricing data."""
89
+ self._cache[key] = {"data": data, "timestamp": time.time()}
90
+ self._save_cache()
91
+
92
+ def get_model_pricing(self, model_name: str) -> Optional[Dict[str, float]]:
93
+ """Get cached pricing for a specific model."""
94
+ return self.get(f"model:{model_name}")
95
+
96
+ def set_model_pricing(self, model_name: str, pricing: Dict[str, float]):
97
+ """Cache pricing for a specific model."""
98
+ self.set(f"model:{model_name}", pricing)
99
+
100
+ def get_all_pricing(self) -> Optional[Dict[str, Dict[str, float]]]:
101
+ """Get cached pricing for all models."""
102
+ return self.get("all_models")
103
+
104
+ def set_all_pricing(self, pricing: Dict[str, Dict[str, float]]):
105
+ """Cache pricing for all models."""
106
+ self.set("all_models", pricing)
107
+
108
+ def clear(self):
109
+ """Clear all cached data."""
110
+ self._cache = {}
111
+ self._save_cache()
112
+ logger.info("Pricing cache cleared")
113
+
114
+ def clear_expired(self):
115
+ """Remove expired cache entries."""
116
+ expired_keys = []
117
+ for key, entry in self._cache.items():
118
+ if self._is_expired(entry["timestamp"]):
119
+ expired_keys.append(key)
120
+
121
+ for key in expired_keys:
122
+ del self._cache[key]
123
+
124
+ if expired_keys:
125
+ self._save_cache()
126
+ logger.info(f"Removed {len(expired_keys)} expired cache entries")
127
+
128
+ def get_cache_stats(self) -> Dict[str, Any]:
129
+ """Get cache statistics."""
130
+ total_entries = len(self._cache)
131
+ expired_count = 0
132
+
133
+ for entry in self._cache.values():
134
+ if self._is_expired(entry["timestamp"]):
135
+ expired_count += 1
136
+
137
+ return {
138
+ "total_entries": total_entries,
139
+ "expired_entries": expired_count,
140
+ "valid_entries": total_entries - expired_count,
141
+ "cache_file": str(self.cache_file),
142
+ "cache_ttl": self.cache_ttl,
143
+ }
@@ -0,0 +1,240 @@
1
+ """
2
+ LLM Pricing Data Fetcher
3
+
4
+ Fetches real-time pricing data from various LLM providers.
5
+ Supports multiple providers and fallback to static pricing.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+ import aiohttp
11
+ from loguru import logger
12
+
13
+
14
+ class PricingFetcher:
15
+ """Fetches LLM pricing data from various sources."""
16
+
17
+ def __init__(self):
18
+ self.session = None
19
+ self.static_pricing = self._load_static_pricing()
20
+
21
+ async def __aenter__(self):
22
+ self.session = aiohttp.ClientSession()
23
+ return self
24
+
25
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
26
+ if self.session:
27
+ await self.session.close()
28
+
29
+ def _load_static_pricing(self) -> Dict[str, Dict[str, float]]:
30
+ """Load static pricing as fallback (per 1K tokens in USD)."""
31
+ return {
32
+ # OpenAI Models
33
+ "gpt-4": {"prompt": 0.03, "completion": 0.06},
34
+ "gpt-4-turbo": {"prompt": 0.01, "completion": 0.03},
35
+ "gpt-4o": {"prompt": 0.005, "completion": 0.015},
36
+ "gpt-4o-mini": {"prompt": 0.00015, "completion": 0.0006},
37
+ "gpt-3.5-turbo": {"prompt": 0.001, "completion": 0.002},
38
+ # Anthropic Models
39
+ "claude-3-opus": {"prompt": 0.015, "completion": 0.075},
40
+ "claude-3-sonnet": {"prompt": 0.003, "completion": 0.015},
41
+ "claude-3-haiku": {"prompt": 0.00025, "completion": 0.00125},
42
+ "claude-3-5-sonnet": {"prompt": 0.003, "completion": 0.015},
43
+ # Google Models
44
+ "gemini-pro": {"prompt": 0.0005, "completion": 0.0015},
45
+ "gemini-pro-vision": {"prompt": 0.0005, "completion": 0.0015},
46
+ "gemini-1.5-pro": {"prompt": 0.0035, "completion": 0.0105},
47
+ "gemini-1.5-flash": {"prompt": 0.00035, "completion": 0.00105},
48
+ # Local/Open Source (free)
49
+ "ollama": {"prompt": 0.0, "completion": 0.0},
50
+ "llama": {"prompt": 0.0, "completion": 0.0},
51
+ "mistral": {"prompt": 0.0, "completion": 0.0},
52
+ "gemma": {"prompt": 0.0, "completion": 0.0},
53
+ "qwen": {"prompt": 0.0, "completion": 0.0},
54
+ "codellama": {"prompt": 0.0, "completion": 0.0},
55
+ "vicuna": {"prompt": 0.0, "completion": 0.0},
56
+ "alpaca": {"prompt": 0.0, "completion": 0.0},
57
+ "vllm": {"prompt": 0.0, "completion": 0.0},
58
+ "lmstudio": {"prompt": 0.0, "completion": 0.0},
59
+ "llamacpp": {"prompt": 0.0, "completion": 0.0},
60
+ }
61
+
62
+ async def fetch_openai_pricing(self) -> Optional[Dict[str, Any]]:
63
+ """Fetch OpenAI pricing from their API (if available)."""
64
+ try:
65
+ # Note: OpenAI doesn't have a public pricing API
66
+ # This would need to be web scraping or manual updates
67
+ logger.info("Using static OpenAI pricing (no public API available)")
68
+ return None
69
+ except Exception as e:
70
+ logger.warning(f"Failed to fetch OpenAI pricing: {e}")
71
+ return None
72
+
73
+ async def fetch_anthropic_pricing(self) -> Optional[Dict[str, Any]]:
74
+ """Fetch Anthropic pricing."""
75
+ try:
76
+ # Note: Anthropic doesn't have a public pricing API
77
+ # This would need to be web scraping or manual updates
78
+ logger.info(
79
+ "Using static Anthropic pricing (no public API available)"
80
+ )
81
+ return None
82
+ except Exception as e:
83
+ logger.warning(f"Failed to fetch Anthropic pricing: {e}")
84
+ return None
85
+
86
+ async def fetch_google_pricing(self) -> Optional[Dict[str, Any]]:
87
+ """Fetch Google/Gemini pricing."""
88
+ try:
89
+ # Note: Google doesn't have a dedicated pricing API for individual models
90
+ # This would need to be web scraping or manual updates
91
+ logger.info("Using static Google pricing (no public API available)")
92
+ return None
93
+ except Exception as e:
94
+ logger.warning(f"Failed to fetch Google pricing: {e}")
95
+ return None
96
+
97
+ async def fetch_huggingface_pricing(self) -> Optional[Dict[str, Any]]:
98
+ """Fetch HuggingFace Inference API pricing."""
99
+ try:
100
+ if not self.session:
101
+ return None
102
+
103
+ # HuggingFace has some pricing info but not a structured API
104
+ # This is more for hosted inference endpoints
105
+ url = "https://huggingface.co/pricing"
106
+ async with self.session.get(url) as response:
107
+ if response.status == 200:
108
+ # Would need to parse HTML for pricing info
109
+ logger.info(
110
+ "HuggingFace pricing would require HTML parsing"
111
+ )
112
+ return None
113
+ except Exception as e:
114
+ logger.warning(f"Failed to fetch HuggingFace pricing: {e}")
115
+ return None
116
+
117
+ async def get_model_pricing(
118
+ self, model_name: str, provider: str = None
119
+ ) -> Optional[Dict[str, float]]:
120
+ """Get pricing for a specific model and provider."""
121
+ # Normalize inputs
122
+ model_name = model_name.lower() if model_name else ""
123
+ provider = provider.lower() if provider else ""
124
+
125
+ # Provider-first approach: Check if provider indicates local/free models
126
+ local_providers = ["ollama", "vllm", "lmstudio", "llamacpp"]
127
+ if provider in local_providers:
128
+ logger.debug(
129
+ f"Local provider '{provider}' detected - returning zero cost"
130
+ )
131
+ return {"prompt": 0.0, "completion": 0.0}
132
+
133
+ # Try to fetch live pricing first (most providers don't have APIs)
134
+ if (
135
+ provider == "openai"
136
+ or "gpt" in model_name
137
+ or "openai" in model_name
138
+ ):
139
+ await self.fetch_openai_pricing()
140
+ elif (
141
+ provider == "anthropic"
142
+ or "claude" in model_name
143
+ or "anthropic" in model_name
144
+ ):
145
+ await self.fetch_anthropic_pricing()
146
+ elif (
147
+ provider == "google"
148
+ or "gemini" in model_name
149
+ or "google" in model_name
150
+ ):
151
+ await self.fetch_google_pricing()
152
+
153
+ # Fallback to static pricing with provider priority
154
+ if provider:
155
+ # First try provider-specific lookup with exact matching
156
+ provider_models = self._get_models_by_provider(provider)
157
+ # Try exact match
158
+ if model_name in provider_models:
159
+ return provider_models[model_name]
160
+ # Try exact match without provider prefix
161
+ if "/" in model_name:
162
+ model_only = model_name.split("/")[-1]
163
+ if model_only in provider_models:
164
+ return provider_models[model_only]
165
+
166
+ # Exact model name matching only
167
+ # First try exact match
168
+ if model_name in self.static_pricing:
169
+ return self.static_pricing[model_name]
170
+
171
+ # Try exact match without provider prefix (e.g., "openai/gpt-4o-mini" -> "gpt-4o-mini")
172
+ if "/" in model_name:
173
+ model_only = model_name.split("/")[-1]
174
+ if model_only in self.static_pricing:
175
+ return self.static_pricing[model_only]
176
+
177
+ # No pricing found - return None instead of default pricing
178
+ logger.warning(
179
+ f"No pricing found for model: {model_name}, provider: {provider}"
180
+ )
181
+ return None
182
+
183
+ def _get_models_by_provider(
184
+ self, provider: str
185
+ ) -> Dict[str, Dict[str, float]]:
186
+ """Get models for a specific provider."""
187
+ provider = provider.lower()
188
+ provider_models = {}
189
+
190
+ if provider == "openai":
191
+ provider_models = {
192
+ k: v
193
+ for k, v in self.static_pricing.items()
194
+ if k.startswith("gpt")
195
+ }
196
+ elif provider == "anthropic":
197
+ provider_models = {
198
+ k: v
199
+ for k, v in self.static_pricing.items()
200
+ if k.startswith("claude")
201
+ }
202
+ elif provider == "google":
203
+ provider_models = {
204
+ k: v
205
+ for k, v in self.static_pricing.items()
206
+ if k.startswith("gemini")
207
+ }
208
+ elif provider in ["ollama", "vllm", "lmstudio", "llamacpp"]:
209
+ # All local models are free
210
+ provider_models = {
211
+ k: v
212
+ for k, v in self.static_pricing.items()
213
+ if v["prompt"] == 0.0 and v["completion"] == 0.0
214
+ }
215
+
216
+ return provider_models
217
+
218
+ async def get_all_pricing(self) -> Dict[str, Dict[str, float]]:
219
+ """Get pricing for all known models."""
220
+ # In the future, this could aggregate from multiple live sources
221
+ return self.static_pricing.copy()
222
+
223
+ def get_provider_from_model(self, model_name: str) -> str:
224
+ """Determine the provider from model name."""
225
+ model_name = model_name.lower()
226
+
227
+ if "gpt" in model_name or "openai" in model_name:
228
+ return "openai"
229
+ elif "claude" in model_name or "anthropic" in model_name:
230
+ return "anthropic"
231
+ elif "gemini" in model_name or "google" in model_name:
232
+ return "google"
233
+ elif "llama" in model_name or "meta" in model_name:
234
+ return "meta"
235
+ elif "mistral" in model_name:
236
+ return "mistral"
237
+ elif "ollama" in model_name:
238
+ return "ollama"
239
+ else:
240
+ return "unknown"
@@ -0,0 +1,51 @@
1
+ """Common query utilities for metrics module."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Any
5
+
6
+ from sqlalchemy import Column
7
+
8
+
9
+ def get_time_filter_condition(period: str, timestamp_column: Column) -> Any:
10
+ """Get SQLAlchemy condition for time filtering.
11
+
12
+ Args:
13
+ period: Time period ('7d', '30d', '3m', '1y', 'all')
14
+ timestamp_column: SQLAlchemy timestamp column to filter on
15
+
16
+ Returns:
17
+ SQLAlchemy condition object or None for 'all'
18
+ """
19
+ if period == "all":
20
+ return None
21
+ elif period == "7d":
22
+ cutoff = datetime.now() - timedelta(days=7)
23
+ elif period == "30d":
24
+ cutoff = datetime.now() - timedelta(days=30)
25
+ elif period == "3m":
26
+ cutoff = datetime.now() - timedelta(days=90)
27
+ elif period == "1y":
28
+ cutoff = datetime.now() - timedelta(days=365)
29
+ else:
30
+ # Default to 30 days for unknown periods
31
+ cutoff = datetime.now() - timedelta(days=30)
32
+
33
+ return timestamp_column >= cutoff
34
+
35
+
36
+ def get_research_mode_condition(research_mode: str, mode_column: Column) -> Any:
37
+ """Get SQLAlchemy condition for research mode filtering.
38
+
39
+ Args:
40
+ research_mode: Research mode ('quick', 'detailed', 'all')
41
+ mode_column: SQLAlchemy column to filter on
42
+
43
+ Returns:
44
+ SQLAlchemy condition object or None for 'all'
45
+ """
46
+ if research_mode == "all":
47
+ return None
48
+ elif research_mode in ["quick", "detailed"]:
49
+ return mode_column == research_mode
50
+ else:
51
+ return None