Coverage for src/local_deep_research/metrics/pricing/pricing_fetcher.py: 98%
68 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""
2LLM Pricing Data Fetcher
4Fetches real-time pricing data from various LLM providers.
5Supports multiple providers and fallback to static pricing.
6"""
8from typing import Dict, Optional
10import aiohttp
11from loguru import logger
13from ...llm.providers.base import normalize_provider
16class PricingFetcher:
17 """Fetches LLM pricing data from various sources."""
19 def __init__(self):
20 self.session = None
21 self.static_pricing = self._load_static_pricing()
23 async def __aenter__(self):
24 self.session = aiohttp.ClientSession()
25 return self
27 async def __aexit__(self, exc_type, exc_val, exc_tb):
28 if self.session:
29 await self.session.close()
31 def _load_static_pricing(self) -> Dict[str, Dict[str, float]]:
32 """Load static pricing as fallback (per 1K tokens in USD)."""
33 return {
34 # OpenAI Models
35 "gpt-4": {"prompt": 0.03, "completion": 0.06},
36 "gpt-4-turbo": {"prompt": 0.01, "completion": 0.03},
37 "gpt-4o": {"prompt": 0.005, "completion": 0.015},
38 "gpt-4o-mini": {"prompt": 0.00015, "completion": 0.0006},
39 "gpt-3.5-turbo": {"prompt": 0.001, "completion": 0.002},
40 # Anthropic Models
41 "claude-3-opus": {"prompt": 0.015, "completion": 0.075},
42 "claude-3-sonnet": {"prompt": 0.003, "completion": 0.015},
43 "claude-3-haiku": {"prompt": 0.00025, "completion": 0.00125},
44 "claude-3-5-sonnet": {"prompt": 0.003, "completion": 0.015},
45 # Google Models
46 "gemini-pro": {"prompt": 0.0005, "completion": 0.0015},
47 "gemini-pro-vision": {"prompt": 0.0005, "completion": 0.0015},
48 "gemini-1.5-pro": {"prompt": 0.0035, "completion": 0.0105},
49 "gemini-1.5-flash": {"prompt": 0.00035, "completion": 0.00105},
50 # Local/Open Source (free)
51 "ollama": {"prompt": 0.0, "completion": 0.0},
52 "llama": {"prompt": 0.0, "completion": 0.0},
53 "mistral": {"prompt": 0.0, "completion": 0.0},
54 "gemma": {"prompt": 0.0, "completion": 0.0},
55 "qwen": {"prompt": 0.0, "completion": 0.0},
56 "codellama": {"prompt": 0.0, "completion": 0.0},
57 "vicuna": {"prompt": 0.0, "completion": 0.0},
58 "alpaca": {"prompt": 0.0, "completion": 0.0},
59 "lmstudio": {"prompt": 0.0, "completion": 0.0},
60 "llamacpp": {"prompt": 0.0, "completion": 0.0},
61 }
63 async def get_model_pricing(
64 self, model_name: str, provider: str = None
65 ) -> Optional[Dict[str, float]]:
66 """Get pricing for a specific model and provider."""
67 return self.get_static_model_pricing(model_name, provider)
69 def get_static_model_pricing(
70 self, model_name: str, provider: Optional[str] = None
71 ) -> Optional[Dict[str, float]]:
72 """Resolve provider-aware fallback pricing without network I/O."""
73 # Normalize inputs
74 model_name = model_name.lower() if model_name else ""
75 provider = normalize_provider(provider) or ""
77 # Provider-first approach: Check if provider indicates local/free models
78 local_providers = ["ollama", "lmstudio", "llamacpp"]
79 if provider in local_providers:
80 logger.debug(
81 f"Local provider '{provider}' detected - returning zero cost"
82 )
83 return {"prompt": 0.0, "completion": 0.0}
85 # Fallback to static pricing with provider priority
86 if provider:
87 # First try provider-specific lookup with exact matching
88 provider_models = self._get_models_by_provider(provider)
89 # Try exact match
90 if model_name in provider_models:
91 return provider_models[model_name]
92 # Try exact match without provider prefix
93 if "/" in model_name:
94 model_only = model_name.split("/")[-1]
95 if model_only in provider_models:
96 return provider_models[model_only]
98 # Exact model name matching only
99 # First try exact match
100 if model_name in self.static_pricing:
101 return self.static_pricing[model_name]
103 # Try exact match without provider prefix (e.g., "openai/gpt-4o-mini" -> "gpt-4o-mini")
104 if "/" in model_name:
105 model_only = model_name.split("/")[-1]
106 if model_only in self.static_pricing:
107 return self.static_pricing[model_only]
109 # No pricing found - return None instead of default pricing
110 logger.warning(
111 f"No pricing found for model: {model_name}, provider: {provider}"
112 )
113 return None
115 def _get_models_by_provider(
116 self, provider: str
117 ) -> Dict[str, Dict[str, float]]:
118 """Get models for a specific provider."""
119 provider = normalize_provider(provider) or ""
120 provider_models = {}
122 if provider == "openai":
123 provider_models = {
124 k: v
125 for k, v in self.static_pricing.items()
126 if k.startswith("gpt")
127 }
128 elif provider == "anthropic":
129 provider_models = {
130 k: v
131 for k, v in self.static_pricing.items()
132 if k.startswith("claude")
133 }
134 elif provider == "google":
135 provider_models = {
136 k: v
137 for k, v in self.static_pricing.items()
138 if k.startswith("gemini")
139 }
140 elif provider in ["ollama", "lmstudio", "llamacpp"]:
141 # All local models are free
142 provider_models = {
143 k: v
144 for k, v in self.static_pricing.items()
145 if v["prompt"] == 0.0 and v["completion"] == 0.0
146 }
148 return provider_models
150 def get_provider_from_model(self, model_name: str) -> str:
151 """Determine the provider from model name."""
152 model_name = model_name.lower()
154 if "gpt" in model_name or "openai" in model_name:
155 return "openai"
156 if "claude" in model_name or "anthropic" in model_name:
157 return "anthropic"
158 if "gemini" in model_name or "google" in model_name:
159 return "google"
160 if "llama" in model_name or "meta" in model_name:
161 return "meta"
162 if "mistral" in model_name:
163 return "mistral"
164 if "ollama" in model_name: 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true
165 return "ollama"
166 return "unknown"