local-deep-research 0.4.4__py3-none-any.whl → 0.5.0__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.
- local_deep_research/__init__.py +7 -0
- local_deep_research/__version__.py +1 -1
- local_deep_research/advanced_search_system/answer_decoding/__init__.py +5 -0
- local_deep_research/advanced_search_system/answer_decoding/browsecomp_answer_decoder.py +421 -0
- local_deep_research/advanced_search_system/candidate_exploration/README.md +219 -0
- local_deep_research/advanced_search_system/candidate_exploration/__init__.py +25 -0
- local_deep_research/advanced_search_system/candidate_exploration/adaptive_explorer.py +329 -0
- local_deep_research/advanced_search_system/candidate_exploration/base_explorer.py +341 -0
- local_deep_research/advanced_search_system/candidate_exploration/constraint_guided_explorer.py +436 -0
- local_deep_research/advanced_search_system/candidate_exploration/diversity_explorer.py +457 -0
- local_deep_research/advanced_search_system/candidate_exploration/parallel_explorer.py +250 -0
- local_deep_research/advanced_search_system/candidate_exploration/progressive_explorer.py +255 -0
- local_deep_research/advanced_search_system/candidates/__init__.py +5 -0
- local_deep_research/advanced_search_system/candidates/base_candidate.py +59 -0
- local_deep_research/advanced_search_system/constraint_checking/README.md +150 -0
- local_deep_research/advanced_search_system/constraint_checking/__init__.py +35 -0
- local_deep_research/advanced_search_system/constraint_checking/base_constraint_checker.py +122 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_checker.py +223 -0
- local_deep_research/advanced_search_system/constraint_checking/constraint_satisfaction_tracker.py +387 -0
- local_deep_research/advanced_search_system/constraint_checking/dual_confidence_checker.py +424 -0
- local_deep_research/advanced_search_system/constraint_checking/evidence_analyzer.py +174 -0
- local_deep_research/advanced_search_system/constraint_checking/intelligent_constraint_relaxer.py +503 -0
- local_deep_research/advanced_search_system/constraint_checking/rejection_engine.py +143 -0
- local_deep_research/advanced_search_system/constraint_checking/strict_checker.py +259 -0
- local_deep_research/advanced_search_system/constraint_checking/threshold_checker.py +213 -0
- local_deep_research/advanced_search_system/constraints/__init__.py +6 -0
- local_deep_research/advanced_search_system/constraints/base_constraint.py +58 -0
- local_deep_research/advanced_search_system/constraints/constraint_analyzer.py +143 -0
- local_deep_research/advanced_search_system/evidence/__init__.py +12 -0
- local_deep_research/advanced_search_system/evidence/base_evidence.py +57 -0
- local_deep_research/advanced_search_system/evidence/evaluator.py +159 -0
- local_deep_research/advanced_search_system/evidence/requirements.py +122 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +3 -1
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +8 -2
- local_deep_research/advanced_search_system/filters/journal_reputation_filter.py +43 -29
- local_deep_research/advanced_search_system/findings/repository.py +54 -17
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +3 -1
- local_deep_research/advanced_search_system/query_generation/adaptive_query_generator.py +405 -0
- local_deep_research/advanced_search_system/questions/__init__.py +16 -0
- local_deep_research/advanced_search_system/questions/atomic_fact_question.py +171 -0
- local_deep_research/advanced_search_system/questions/browsecomp_question.py +287 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +13 -4
- local_deep_research/advanced_search_system/questions/entity_aware_question.py +184 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +9 -3
- local_deep_research/advanced_search_system/search_optimization/cross_constraint_manager.py +624 -0
- local_deep_research/advanced_search_system/source_management/diversity_manager.py +613 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +42 -0
- local_deep_research/advanced_search_system/strategies/adaptive_decomposition_strategy.py +564 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +4 -4
- local_deep_research/advanced_search_system/strategies/browsecomp_entity_strategy.py +1031 -0
- local_deep_research/advanced_search_system/strategies/browsecomp_optimized_strategy.py +778 -0
- local_deep_research/advanced_search_system/strategies/concurrent_dual_confidence_strategy.py +446 -0
- local_deep_research/advanced_search_system/strategies/constrained_search_strategy.py +1348 -0
- local_deep_research/advanced_search_system/strategies/constraint_parallel_strategy.py +522 -0
- local_deep_research/advanced_search_system/strategies/direct_search_strategy.py +217 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_strategy.py +320 -0
- local_deep_research/advanced_search_system/strategies/dual_confidence_with_rejection.py +219 -0
- local_deep_research/advanced_search_system/strategies/early_stop_constrained_strategy.py +369 -0
- local_deep_research/advanced_search_system/strategies/entity_aware_source_strategy.py +140 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy.py +1248 -0
- local_deep_research/advanced_search_system/strategies/evidence_based_strategy_v2.py +1337 -0
- local_deep_research/advanced_search_system/strategies/focused_iteration_strategy.py +537 -0
- local_deep_research/advanced_search_system/strategies/improved_evidence_based_strategy.py +782 -0
- local_deep_research/advanced_search_system/strategies/iterative_reasoning_strategy.py +760 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +55 -21
- local_deep_research/advanced_search_system/strategies/llm_driven_modular_strategy.py +865 -0
- local_deep_research/advanced_search_system/strategies/modular_strategy.py +1142 -0
- local_deep_research/advanced_search_system/strategies/parallel_constrained_strategy.py +506 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +34 -16
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +29 -9
- local_deep_research/advanced_search_system/strategies/recursive_decomposition_strategy.py +492 -0
- local_deep_research/advanced_search_system/strategies/smart_decomposition_strategy.py +284 -0
- local_deep_research/advanced_search_system/strategies/smart_query_strategy.py +515 -0
- local_deep_research/advanced_search_system/strategies/source_based_strategy.py +48 -24
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +34 -14
- local_deep_research/advanced_search_system/tools/base_tool.py +7 -2
- local_deep_research/api/benchmark_functions.py +6 -2
- local_deep_research/api/research_functions.py +10 -4
- local_deep_research/benchmarks/__init__.py +9 -7
- local_deep_research/benchmarks/benchmark_functions.py +6 -2
- local_deep_research/benchmarks/cli/benchmark_commands.py +27 -10
- local_deep_research/benchmarks/cli.py +38 -13
- local_deep_research/benchmarks/comparison/__init__.py +4 -2
- local_deep_research/benchmarks/comparison/evaluator.py +316 -239
- local_deep_research/benchmarks/datasets/__init__.py +1 -1
- local_deep_research/benchmarks/datasets/base.py +91 -72
- local_deep_research/benchmarks/datasets/browsecomp.py +54 -33
- local_deep_research/benchmarks/datasets/custom_dataset_template.py +19 -19
- local_deep_research/benchmarks/datasets/simpleqa.py +14 -14
- local_deep_research/benchmarks/datasets/utils.py +48 -29
- local_deep_research/benchmarks/datasets.py +4 -11
- local_deep_research/benchmarks/efficiency/__init__.py +8 -4
- local_deep_research/benchmarks/efficiency/resource_monitor.py +223 -171
- local_deep_research/benchmarks/efficiency/speed_profiler.py +62 -48
- local_deep_research/benchmarks/evaluators/browsecomp.py +3 -1
- local_deep_research/benchmarks/evaluators/composite.py +6 -2
- local_deep_research/benchmarks/evaluators/simpleqa.py +36 -13
- local_deep_research/benchmarks/graders.py +32 -10
- local_deep_research/benchmarks/metrics/README.md +1 -1
- local_deep_research/benchmarks/metrics/calculation.py +25 -10
- local_deep_research/benchmarks/metrics/reporting.py +7 -3
- local_deep_research/benchmarks/metrics/visualization.py +42 -23
- local_deep_research/benchmarks/metrics.py +1 -1
- local_deep_research/benchmarks/optimization/__init__.py +3 -1
- local_deep_research/benchmarks/optimization/api.py +7 -1
- local_deep_research/benchmarks/optimization/optuna_optimizer.py +75 -26
- local_deep_research/benchmarks/runners.py +48 -15
- local_deep_research/citation_handler.py +65 -92
- local_deep_research/citation_handlers/__init__.py +15 -0
- local_deep_research/citation_handlers/base_citation_handler.py +70 -0
- local_deep_research/citation_handlers/forced_answer_citation_handler.py +179 -0
- local_deep_research/citation_handlers/precision_extraction_handler.py +550 -0
- local_deep_research/citation_handlers/standard_citation_handler.py +80 -0
- local_deep_research/config/llm_config.py +271 -169
- local_deep_research/config/search_config.py +14 -5
- local_deep_research/defaults/__init__.py +0 -1
- local_deep_research/metrics/__init__.py +13 -0
- local_deep_research/metrics/database.py +58 -0
- local_deep_research/metrics/db_models.py +115 -0
- local_deep_research/metrics/migrate_add_provider_to_token_usage.py +148 -0
- local_deep_research/metrics/migrate_call_stack_tracking.py +105 -0
- local_deep_research/metrics/migrate_enhanced_tracking.py +75 -0
- local_deep_research/metrics/migrate_research_ratings.py +31 -0
- local_deep_research/metrics/models.py +61 -0
- local_deep_research/metrics/pricing/__init__.py +12 -0
- local_deep_research/metrics/pricing/cost_calculator.py +237 -0
- local_deep_research/metrics/pricing/pricing_cache.py +143 -0
- local_deep_research/metrics/pricing/pricing_fetcher.py +240 -0
- local_deep_research/metrics/query_utils.py +51 -0
- local_deep_research/metrics/search_tracker.py +380 -0
- local_deep_research/metrics/token_counter.py +1078 -0
- local_deep_research/migrate_db.py +3 -1
- local_deep_research/report_generator.py +22 -8
- local_deep_research/search_system.py +390 -9
- local_deep_research/test_migration.py +15 -5
- local_deep_research/utilities/db_utils.py +7 -4
- local_deep_research/utilities/es_utils.py +115 -104
- local_deep_research/utilities/llm_utils.py +15 -5
- local_deep_research/utilities/log_utils.py +151 -0
- local_deep_research/utilities/search_cache.py +387 -0
- local_deep_research/utilities/search_utilities.py +14 -6
- local_deep_research/utilities/threading_utils.py +92 -0
- local_deep_research/utilities/url_utils.py +6 -0
- local_deep_research/web/api.py +347 -0
- local_deep_research/web/app.py +13 -17
- local_deep_research/web/app_factory.py +71 -66
- local_deep_research/web/database/migrate_to_ldr_db.py +12 -4
- local_deep_research/web/database/migrations.py +5 -3
- local_deep_research/web/database/models.py +51 -2
- local_deep_research/web/database/schema_upgrade.py +49 -29
- local_deep_research/web/models/database.py +51 -61
- local_deep_research/web/routes/api_routes.py +56 -22
- local_deep_research/web/routes/benchmark_routes.py +4 -1
- local_deep_research/web/routes/globals.py +22 -0
- local_deep_research/web/routes/history_routes.py +71 -46
- local_deep_research/web/routes/metrics_routes.py +1155 -0
- local_deep_research/web/routes/research_routes.py +227 -41
- local_deep_research/web/routes/settings_routes.py +156 -55
- local_deep_research/web/services/research_service.py +310 -103
- local_deep_research/web/services/resource_service.py +36 -11
- local_deep_research/web/services/settings_manager.py +55 -17
- local_deep_research/web/services/settings_service.py +12 -4
- local_deep_research/web/services/socket_service.py +295 -188
- local_deep_research/web/static/css/custom_dropdown.css +180 -0
- local_deep_research/web/static/css/styles.css +39 -1
- local_deep_research/web/static/js/components/detail.js +633 -267
- local_deep_research/web/static/js/components/details.js +751 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +11 -11
- local_deep_research/web/static/js/components/fallback/ui.js +23 -23
- local_deep_research/web/static/js/components/history.js +76 -76
- local_deep_research/web/static/js/components/logpanel.js +61 -13
- local_deep_research/web/static/js/components/progress.js +13 -2
- local_deep_research/web/static/js/components/research.js +99 -12
- local_deep_research/web/static/js/components/results.js +239 -106
- local_deep_research/web/static/js/main.js +40 -40
- local_deep_research/web/static/js/services/audio.js +1 -1
- local_deep_research/web/static/js/services/formatting.js +11 -11
- local_deep_research/web/static/js/services/keyboard.js +157 -0
- local_deep_research/web/static/js/services/pdf.js +80 -80
- local_deep_research/web/static/sounds/README.md +1 -1
- local_deep_research/web/templates/base.html +1 -0
- local_deep_research/web/templates/components/log_panel.html +7 -1
- local_deep_research/web/templates/components/mobile_nav.html +1 -1
- local_deep_research/web/templates/components/sidebar.html +3 -0
- local_deep_research/web/templates/pages/cost_analytics.html +1245 -0
- local_deep_research/web/templates/pages/details.html +325 -24
- local_deep_research/web/templates/pages/history.html +1 -1
- local_deep_research/web/templates/pages/metrics.html +1929 -0
- local_deep_research/web/templates/pages/progress.html +2 -2
- local_deep_research/web/templates/pages/research.html +53 -17
- local_deep_research/web/templates/pages/results.html +12 -1
- local_deep_research/web/templates/pages/star_reviews.html +803 -0
- local_deep_research/web/utils/formatters.py +9 -3
- local_deep_research/web_search_engines/default_search_engines.py +5 -3
- local_deep_research/web_search_engines/engines/full_search.py +8 -2
- local_deep_research/web_search_engines/engines/meta_search_engine.py +59 -20
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +19 -6
- local_deep_research/web_search_engines/engines/search_engine_brave.py +6 -2
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +3 -1
- local_deep_research/web_search_engines/engines/search_engine_elasticsearch.py +81 -58
- local_deep_research/web_search_engines/engines/search_engine_github.py +46 -15
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +16 -6
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +39 -15
- local_deep_research/web_search_engines/engines/search_engine_local.py +58 -25
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +15 -5
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +63 -21
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +37 -11
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +27 -9
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +12 -4
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +31 -10
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +12 -3
- local_deep_research/web_search_engines/search_engine_base.py +83 -35
- local_deep_research/web_search_engines/search_engine_factory.py +25 -8
- local_deep_research/web_search_engines/search_engines_config.py +9 -3
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/METADATA +7 -1
- local_deep_research-0.5.0.dist-info/RECORD +265 -0
- local_deep_research-0.4.4.dist-info/RECORD +0 -177
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/WHEEL +0 -0
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.dist-info}/entry_points.txt +0 -0
- {local_deep_research-0.4.4.dist-info → local_deep_research-0.5.0.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
|