Coverage for src/local_deep_research/metrics/pricing/pricing_fetcher.py: 95%
66 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-03 23:15 +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 # Normalize inputs
68 model_name = model_name.lower() if model_name else ""
69 provider = normalize_provider(provider) or ""
71 # Provider-first approach: Check if provider indicates local/free models
72 local_providers = ["ollama", "lmstudio", "llamacpp"]
73 if provider in local_providers:
74 logger.debug(
75 f"Local provider '{provider}' detected - returning zero cost"
76 )
77 return {"prompt": 0.0, "completion": 0.0}
79 # Fallback to static pricing with provider priority
80 if provider:
81 # First try provider-specific lookup with exact matching
82 provider_models = self._get_models_by_provider(provider)
83 # Try exact match
84 if model_name in provider_models:
85 return provider_models[model_name]
86 # Try exact match without provider prefix
87 if "/" in model_name: 87 ↛ 94line 87 didn't jump to line 94 because the condition on line 87 was always true
88 model_only = model_name.split("/")[-1]
89 if model_only in provider_models: 89 ↛ 94line 89 didn't jump to line 94 because the condition on line 89 was always true
90 return provider_models[model_only]
92 # Exact model name matching only
93 # First try exact match
94 if model_name in self.static_pricing:
95 return self.static_pricing[model_name]
97 # Try exact match without provider prefix (e.g., "openai/gpt-4o-mini" -> "gpt-4o-mini")
98 if "/" in model_name:
99 model_only = model_name.split("/")[-1]
100 if model_only in self.static_pricing: 100 ↛ 104line 100 didn't jump to line 104 because the condition on line 100 was always true
101 return self.static_pricing[model_only]
103 # No pricing found - return None instead of default pricing
104 logger.warning(
105 f"No pricing found for model: {model_name}, provider: {provider}"
106 )
107 return None
109 def _get_models_by_provider(
110 self, provider: str
111 ) -> Dict[str, Dict[str, float]]:
112 """Get models for a specific provider."""
113 provider = normalize_provider(provider) or ""
114 provider_models = {}
116 if provider == "openai":
117 provider_models = {
118 k: v
119 for k, v in self.static_pricing.items()
120 if k.startswith("gpt")
121 }
122 elif provider == "anthropic":
123 provider_models = {
124 k: v
125 for k, v in self.static_pricing.items()
126 if k.startswith("claude")
127 }
128 elif provider == "google":
129 provider_models = {
130 k: v
131 for k, v in self.static_pricing.items()
132 if k.startswith("gemini")
133 }
134 elif provider in ["ollama", "lmstudio", "llamacpp"]:
135 # All local models are free
136 provider_models = {
137 k: v
138 for k, v in self.static_pricing.items()
139 if v["prompt"] == 0.0 and v["completion"] == 0.0
140 }
142 return provider_models
144 def get_provider_from_model(self, model_name: str) -> str:
145 """Determine the provider from model name."""
146 model_name = model_name.lower()
148 if "gpt" in model_name or "openai" in model_name:
149 return "openai"
150 if "claude" in model_name or "anthropic" in model_name:
151 return "anthropic"
152 if "gemini" in model_name or "google" in model_name:
153 return "google"
154 if "llama" in model_name or "meta" in model_name:
155 return "meta"
156 if "mistral" in model_name:
157 return "mistral"
158 if "ollama" in model_name: 158 ↛ 159line 158 didn't jump to line 159 because the condition on line 158 was never true
159 return "ollama"
160 return "unknown"