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.
@@ -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)
@@ -0,0 +1,3 @@
1
+ ## pydantic-llm-prices
2
+
3
+ Python package for [pydantic-llm-pricing](https://github.com/pydantic/llm-prices)
@@ -0,0 +1,4 @@
1
+ from .calc import sync_calc_price
2
+ from .types import Usage
3
+
4
+ __all__ = 'sync_calc_price', 'Usage'
@@ -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