genai-prices 0__tar.gz
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.
- genai_prices-0/PKG-INFO +13 -0
- genai_prices-0/README.md +3 -0
- genai_prices-0/genai_prices/__init__.py +4 -0
- genai_prices-0/genai_prices/calc.py +227 -0
- genai_prices-0/genai_prices/data.py +19164 -0
- genai_prices-0/genai_prices/py.typed +0 -0
- genai_prices-0/genai_prices/types.py +319 -0
- genai_prices-0/pyproject.toml +19 -0
genai_prices-0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: genai-prices
|
|
3
|
+
Version: 0
|
|
4
|
+
Summary: Database of prices for calling LLM models.
|
|
5
|
+
Author: Samuel Colvin
|
|
6
|
+
Author-email: Samuel Colvin <samuel@pydantic.dev>
|
|
7
|
+
Requires-Dist: pydantic>=2.11.7
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
## pydantic-llm-prices
|
|
12
|
+
|
|
13
|
+
Python package for [pydantic-llm-pricing](https://github.com/pydantic/llm-prices)
|
genai_prices-0/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from contextlib import AsyncExitStack
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Literal, overload
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from . import data, types
|
|
14
|
+
|
|
15
|
+
__all__ = ('sync_calc_price',)
|
|
16
|
+
DEFAULT_PHONE_HOME_TTL = timedelta(hours=1)
|
|
17
|
+
DEFAULT_PHONE_HOME_REQUEST_TIMEOUT = 30
|
|
18
|
+
DEFAULT_PHONE_HOME_URL = 'https://raw.githubusercontent.com/pydantic/llm-pricing/refs/heads/main/prices/data.json'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class PriceCalculation:
|
|
23
|
+
price: Decimal
|
|
24
|
+
provider: types.Provider
|
|
25
|
+
model: types.ModelInfo
|
|
26
|
+
phone_home_timestamp: datetime | None
|
|
27
|
+
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return (
|
|
30
|
+
f'PriceCalculation(price={self.price!r}, '
|
|
31
|
+
f'provider=Provider(id={self.provider.id!r}, name={self.provider.name!r}, ...), '
|
|
32
|
+
f'model=Model(id={self.model.id!r}, name={self.model.name!r}, ...), '
|
|
33
|
+
f'phone_home_timestamp={self.phone_home_timestamp!r})'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def sync_calc_price(
|
|
39
|
+
usage: types.Usage,
|
|
40
|
+
model_ref: str,
|
|
41
|
+
*,
|
|
42
|
+
provider_id: types.ProviderID,
|
|
43
|
+
request_timestamp: datetime | None = None,
|
|
44
|
+
phone_home: bool = False,
|
|
45
|
+
phone_home_client: httpx.Client | None = None,
|
|
46
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
47
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
48
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
49
|
+
) -> PriceCalculation: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@overload
|
|
53
|
+
def sync_calc_price(
|
|
54
|
+
usage: types.Usage,
|
|
55
|
+
model_ref: str,
|
|
56
|
+
*,
|
|
57
|
+
provider_api_url: str,
|
|
58
|
+
request_timestamp: datetime | None = None,
|
|
59
|
+
phone_home: bool = False,
|
|
60
|
+
phone_home_client: httpx.Client | None = None,
|
|
61
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
62
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
63
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
64
|
+
) -> PriceCalculation: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sync_calc_price(
|
|
68
|
+
usage: types.Usage,
|
|
69
|
+
model_ref: str,
|
|
70
|
+
*,
|
|
71
|
+
provider_id: types.ProviderID | None = None,
|
|
72
|
+
provider_api_url: str | None = None,
|
|
73
|
+
request_timestamp: datetime | None = None,
|
|
74
|
+
phone_home: bool = False,
|
|
75
|
+
phone_home_client: httpx.Client | None = None,
|
|
76
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
77
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
78
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
79
|
+
) -> PriceCalculation:
|
|
80
|
+
global _phone_home_snapshot
|
|
81
|
+
|
|
82
|
+
if phone_home:
|
|
83
|
+
if _phone_home_snapshot is None or not _phone_home_snapshot.active(phone_home_data_ttl):
|
|
84
|
+
try:
|
|
85
|
+
if phone_home_client:
|
|
86
|
+
r = phone_home_client.get(phone_home_url, timeout=phone_home_request_timeout)
|
|
87
|
+
else:
|
|
88
|
+
r = httpx.get(phone_home_url, timeout=phone_home_request_timeout)
|
|
89
|
+
r.raise_for_status()
|
|
90
|
+
providers = data.providers_schema.validate_json(r.content)
|
|
91
|
+
except (httpx.HTTPError, ValidationError) as e:
|
|
92
|
+
warnings.warn(f'Failed to phone home to {phone_home_url}: {e}')
|
|
93
|
+
snapshot = _phone_home_snapshot or _local_snapshot
|
|
94
|
+
else:
|
|
95
|
+
snapshot = _phone_home_snapshot = DataSnapshot(providers=providers, source='phone_number')
|
|
96
|
+
else:
|
|
97
|
+
snapshot = _phone_home_snapshot
|
|
98
|
+
else:
|
|
99
|
+
snapshot = _local_snapshot
|
|
100
|
+
|
|
101
|
+
return snapshot.calc(usage, model_ref, provider_id, provider_api_url, request_timestamp)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
async def async_calc_price(
|
|
106
|
+
usage: types.Usage,
|
|
107
|
+
model_ref: str,
|
|
108
|
+
*,
|
|
109
|
+
provider_id: types.ProviderID,
|
|
110
|
+
request_timestamp: datetime | None = None,
|
|
111
|
+
phone_home: bool = False,
|
|
112
|
+
phone_home_client: httpx.AsyncClient | None = None,
|
|
113
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
114
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
115
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
116
|
+
) -> PriceCalculation: ...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
async def async_calc_price(
|
|
121
|
+
usage: types.Usage,
|
|
122
|
+
model_ref: str,
|
|
123
|
+
*,
|
|
124
|
+
provider_api_url: str,
|
|
125
|
+
request_timestamp: datetime | None = None,
|
|
126
|
+
phone_home: bool = False,
|
|
127
|
+
phone_home_client: httpx.AsyncClient | None = None,
|
|
128
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
129
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
130
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
131
|
+
) -> PriceCalculation: ...
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def async_calc_price(
|
|
135
|
+
usage: types.Usage,
|
|
136
|
+
model_ref: str,
|
|
137
|
+
*,
|
|
138
|
+
provider_id: types.ProviderID | None = None,
|
|
139
|
+
provider_api_url: str | None = None,
|
|
140
|
+
request_timestamp: datetime | None = None,
|
|
141
|
+
phone_home: bool = False,
|
|
142
|
+
phone_home_client: httpx.AsyncClient | None = None,
|
|
143
|
+
phone_home_url: str = DEFAULT_PHONE_HOME_URL,
|
|
144
|
+
phone_home_data_ttl: timedelta = DEFAULT_PHONE_HOME_TTL,
|
|
145
|
+
phone_home_request_timeout: int = DEFAULT_PHONE_HOME_REQUEST_TIMEOUT,
|
|
146
|
+
) -> PriceCalculation:
|
|
147
|
+
global _phone_home_snapshot
|
|
148
|
+
|
|
149
|
+
snapshot = _local_snapshot
|
|
150
|
+
if phone_home:
|
|
151
|
+
if _phone_home_snapshot is None or not _phone_home_snapshot.active(phone_home_data_ttl):
|
|
152
|
+
async with AsyncExitStack() as exit_stack:
|
|
153
|
+
try:
|
|
154
|
+
if not phone_home_client:
|
|
155
|
+
phone_home_client = httpx.AsyncClient()
|
|
156
|
+
await exit_stack.enter_async_context(phone_home_client)
|
|
157
|
+
|
|
158
|
+
r = await phone_home_client.get(phone_home_url, timeout=phone_home_request_timeout)
|
|
159
|
+
r.raise_for_status()
|
|
160
|
+
providers = data.providers_schema.validate_json(r.content)
|
|
161
|
+
except (httpx.HTTPError, ValidationError) as e:
|
|
162
|
+
warnings.warn(f'Failed to phone home to {phone_home_url}: {e}')
|
|
163
|
+
snapshot = _phone_home_snapshot or _local_snapshot
|
|
164
|
+
else:
|
|
165
|
+
snapshot = _phone_home_snapshot = DataSnapshot(providers=providers, source='phone_number')
|
|
166
|
+
else:
|
|
167
|
+
snapshot = _phone_home_snapshot
|
|
168
|
+
|
|
169
|
+
return snapshot.calc(usage, model_ref, provider_id, provider_api_url, request_timestamp)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class DataSnapshot:
|
|
174
|
+
providers: list[types.Provider]
|
|
175
|
+
source: Literal['phone_number', 'local']
|
|
176
|
+
_lookup_cache: dict[tuple[str | None, str], tuple[types.Provider, types.ModelInfo]] = field(
|
|
177
|
+
default_factory=lambda: {}
|
|
178
|
+
)
|
|
179
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
180
|
+
|
|
181
|
+
def active(self, ttl: timedelta) -> bool:
|
|
182
|
+
return self.timestamp + ttl > datetime.now()
|
|
183
|
+
|
|
184
|
+
def calc(
|
|
185
|
+
self,
|
|
186
|
+
usage: types.Usage,
|
|
187
|
+
model_ref: str,
|
|
188
|
+
provider_id: types.ProviderID | None,
|
|
189
|
+
provider_api_url: str | None,
|
|
190
|
+
request_timestamp: datetime | None,
|
|
191
|
+
) -> PriceCalculation:
|
|
192
|
+
request_timestamp = request_timestamp or datetime.now(tz=timezone.utc)
|
|
193
|
+
|
|
194
|
+
provider, model = self.find_provider_model(model_ref, provider_id, provider_api_url)
|
|
195
|
+
return PriceCalculation(
|
|
196
|
+
price=model.get_prices(request_timestamp).calc_price(usage),
|
|
197
|
+
provider=provider,
|
|
198
|
+
model=model,
|
|
199
|
+
phone_home_timestamp=self.timestamp if self.source == 'phone_number' else None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def find_provider_model(
|
|
203
|
+
self,
|
|
204
|
+
model_ref: str,
|
|
205
|
+
provider_id: types.ProviderID | None,
|
|
206
|
+
provider_api_url: str | None,
|
|
207
|
+
) -> tuple[types.Provider, types.ModelInfo]:
|
|
208
|
+
if provider_model := self._lookup_cache.get((provider_id or provider_api_url, model_ref)):
|
|
209
|
+
return provider_model
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
provider = next(provider for provider in self.providers if provider.is_match(provider_id, provider_api_url))
|
|
213
|
+
except StopIteration as e:
|
|
214
|
+
if provider_id:
|
|
215
|
+
raise LookupError(f'Unable to find provider {provider_id=!r}') from e
|
|
216
|
+
else:
|
|
217
|
+
raise LookupError(f'Unable to find provider {provider_api_url=!r}') from e
|
|
218
|
+
|
|
219
|
+
if model := provider.find_model(model_ref):
|
|
220
|
+
self._lookup_cache[(provider_id or provider_api_url, model_ref)] = ret = provider, model
|
|
221
|
+
return ret
|
|
222
|
+
else:
|
|
223
|
+
raise LookupError(f'Unable to find model with {model_ref=!r} in {provider.id}')
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_local_snapshot = DataSnapshot(providers=data.providers, source='local')
|
|
227
|
+
_phone_home_snapshot: DataSnapshot | None = None
|