metergraph-core 0.2.61__tar.gz → 0.3.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.
- {metergraph_core-0.2.61/src/metergraph_core.egg-info → metergraph_core-0.3.0}/PKG-INFO +55 -1
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/README.md +54 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/pyproject.toml +1 -1
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/__init__.py +4 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/catalog.py +230 -58
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/data/models.yaml +43 -1
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/data/prices.yaml +131 -2
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/loader.py +65 -1
- {metergraph_core-0.2.61 → metergraph_core-0.3.0/src/metergraph_core.egg-info}/PKG-INFO +55 -1
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/MANIFEST.in +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/setup.cfg +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/billing.py +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/models.py +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metergraph-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reusable MeterGraph catalog and deterministic billing engine
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -92,6 +92,58 @@ prefix, the latter only onto an `aws-bedrock` alias. Geographic prefixes such as
|
|
|
92
92
|
`LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
|
|
93
93
|
checked against its linked provider sources.
|
|
94
94
|
|
|
95
|
+
### Direct lookup and usage cost
|
|
96
|
+
|
|
97
|
+
`lookup_price` returns the price a call is billed at, and `calculate_cost`
|
|
98
|
+
costs a call's usage at that price:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from metergraph_core import ModelUsage
|
|
102
|
+
|
|
103
|
+
found = catalog.lookup_price(
|
|
104
|
+
model="gpt-5.4-mini",
|
|
105
|
+
provider="openai",
|
|
106
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
107
|
+
)
|
|
108
|
+
print(found.status, found.model_id, found.price.id, found.price.currency)
|
|
109
|
+
|
|
110
|
+
result = catalog.calculate_cost(
|
|
111
|
+
model="gpt-5.4-mini",
|
|
112
|
+
provider="openai",
|
|
113
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
114
|
+
usage=ModelUsage(input_tokens=1000, output_tokens=200, cache_read_tokens=500),
|
|
115
|
+
)
|
|
116
|
+
print(result.status, result.model_id, result.cost, result.currency)
|
|
117
|
+
print(result.components.input, result.components.cache_read)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Both resolve exactly as `resolve_price` and `price` do, including
|
|
121
|
+
effective-date windows and the Bedrock fallback, and cost through the same
|
|
122
|
+
arithmetic. `model_id` is the resolved catalog ID, not the alias passed in, and
|
|
123
|
+
`price` is the applied catalog row. An explicit `channel` is authoritative;
|
|
124
|
+
without one, `provider` supplies its direct channel, and with neither the
|
|
125
|
+
result is `unpriced` with `missing_dimensions=("channel",)`.
|
|
126
|
+
|
|
127
|
+
Every `Price` states its `currency`, and `CostResult.currency` is the currency
|
|
128
|
+
of the applied price. The bundled catalog is USD. `cost` and
|
|
129
|
+
`components.input`, `output`, `cache_read`, `cache_write` and `request` are in
|
|
130
|
+
that currency; the older `cost_usd` and `*_usd` names return the same values.
|
|
131
|
+
|
|
132
|
+
A `ModelUsage` count left `None` is unknown, not zero. A missing input or
|
|
133
|
+
output count keeps the subtotal that is known, marks the result `partial`, and
|
|
134
|
+
lists `input_tokens` or `output_tokens` in `missing_dimensions`.
|
|
135
|
+
|
|
136
|
+
`region` selects a regional price and overrides the `region` passed to
|
|
137
|
+
`load_catalog`. When neither names a region:
|
|
138
|
+
|
|
139
|
+
- if every region bills the model the same, the result is `priced`;
|
|
140
|
+
- if a global price applies but some region bills differently, the global
|
|
141
|
+
price is returned as `partial` with `missing_dimensions=("region",)`;
|
|
142
|
+
- if only specific regions price the model, the result is `unpriced` with
|
|
143
|
+
`missing_dimensions=("region",)`.
|
|
144
|
+
|
|
145
|
+
A region the model is not sold in is `unpriced` with nothing missing.
|
|
146
|
+
|
|
95
147
|
## Model registry
|
|
96
148
|
|
|
97
149
|
```python
|
|
@@ -165,7 +217,9 @@ from metergraph_core import (
|
|
|
165
217
|
LoadedCatalog,
|
|
166
218
|
ModelRegistry,
|
|
167
219
|
ModelRoute,
|
|
220
|
+
ModelUsage,
|
|
168
221
|
Price,
|
|
222
|
+
PriceResult,
|
|
169
223
|
ResolvedPrice,
|
|
170
224
|
load_catalog,
|
|
171
225
|
load_model_registry,
|
|
@@ -80,6 +80,58 @@ prefix, the latter only onto an `aws-bedrock` alias. Geographic prefixes such as
|
|
|
80
80
|
`LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
|
|
81
81
|
checked against its linked provider sources.
|
|
82
82
|
|
|
83
|
+
### Direct lookup and usage cost
|
|
84
|
+
|
|
85
|
+
`lookup_price` returns the price a call is billed at, and `calculate_cost`
|
|
86
|
+
costs a call's usage at that price:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from metergraph_core import ModelUsage
|
|
90
|
+
|
|
91
|
+
found = catalog.lookup_price(
|
|
92
|
+
model="gpt-5.4-mini",
|
|
93
|
+
provider="openai",
|
|
94
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
95
|
+
)
|
|
96
|
+
print(found.status, found.model_id, found.price.id, found.price.currency)
|
|
97
|
+
|
|
98
|
+
result = catalog.calculate_cost(
|
|
99
|
+
model="gpt-5.4-mini",
|
|
100
|
+
provider="openai",
|
|
101
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
102
|
+
usage=ModelUsage(input_tokens=1000, output_tokens=200, cache_read_tokens=500),
|
|
103
|
+
)
|
|
104
|
+
print(result.status, result.model_id, result.cost, result.currency)
|
|
105
|
+
print(result.components.input, result.components.cache_read)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Both resolve exactly as `resolve_price` and `price` do, including
|
|
109
|
+
effective-date windows and the Bedrock fallback, and cost through the same
|
|
110
|
+
arithmetic. `model_id` is the resolved catalog ID, not the alias passed in, and
|
|
111
|
+
`price` is the applied catalog row. An explicit `channel` is authoritative;
|
|
112
|
+
without one, `provider` supplies its direct channel, and with neither the
|
|
113
|
+
result is `unpriced` with `missing_dimensions=("channel",)`.
|
|
114
|
+
|
|
115
|
+
Every `Price` states its `currency`, and `CostResult.currency` is the currency
|
|
116
|
+
of the applied price. The bundled catalog is USD. `cost` and
|
|
117
|
+
`components.input`, `output`, `cache_read`, `cache_write` and `request` are in
|
|
118
|
+
that currency; the older `cost_usd` and `*_usd` names return the same values.
|
|
119
|
+
|
|
120
|
+
A `ModelUsage` count left `None` is unknown, not zero. A missing input or
|
|
121
|
+
output count keeps the subtotal that is known, marks the result `partial`, and
|
|
122
|
+
lists `input_tokens` or `output_tokens` in `missing_dimensions`.
|
|
123
|
+
|
|
124
|
+
`region` selects a regional price and overrides the `region` passed to
|
|
125
|
+
`load_catalog`. When neither names a region:
|
|
126
|
+
|
|
127
|
+
- if every region bills the model the same, the result is `priced`;
|
|
128
|
+
- if a global price applies but some region bills differently, the global
|
|
129
|
+
price is returned as `partial` with `missing_dimensions=("region",)`;
|
|
130
|
+
- if only specific regions price the model, the result is `unpriced` with
|
|
131
|
+
`missing_dimensions=("region",)`.
|
|
132
|
+
|
|
133
|
+
A region the model is not sold in is `unpriced` with nothing missing.
|
|
134
|
+
|
|
83
135
|
## Model registry
|
|
84
136
|
|
|
85
137
|
```python
|
|
@@ -153,7 +205,9 @@ from metergraph_core import (
|
|
|
153
205
|
LoadedCatalog,
|
|
154
206
|
ModelRegistry,
|
|
155
207
|
ModelRoute,
|
|
208
|
+
ModelUsage,
|
|
156
209
|
Price,
|
|
210
|
+
PriceResult,
|
|
157
211
|
ResolvedPrice,
|
|
158
212
|
load_catalog,
|
|
159
213
|
load_model_registry,
|
|
@@ -5,7 +5,9 @@ from .catalog import (
|
|
|
5
5
|
Alias,
|
|
6
6
|
CatalogSnapshot,
|
|
7
7
|
CostResult,
|
|
8
|
+
ModelUsage,
|
|
8
9
|
Price,
|
|
10
|
+
PriceResult,
|
|
9
11
|
ResolvedPrice,
|
|
10
12
|
counts_cache_read_in_input,
|
|
11
13
|
direct_channel_for_provider,
|
|
@@ -60,10 +62,12 @@ __all__ = [
|
|
|
60
62
|
"ModelRegistry",
|
|
61
63
|
"ModelRegistryError",
|
|
62
64
|
"ModelRoute",
|
|
65
|
+
"ModelUsage",
|
|
63
66
|
"OfferGroup",
|
|
64
67
|
"ReleaseDate",
|
|
65
68
|
"GatewayBillingEvidence",
|
|
66
69
|
"Price",
|
|
70
|
+
"PriceResult",
|
|
67
71
|
"ResolvedPrice",
|
|
68
72
|
"RetrievalCatalog",
|
|
69
73
|
"RetrievalCostResult",
|
|
@@ -212,6 +212,9 @@ class Price:
|
|
|
212
212
|
# A fixed fee charged once per call on top of tokens, in USD per 1,000
|
|
213
213
|
# calls: a gateway billing a model's built-in search per request.
|
|
214
214
|
request_fee_per_1k_usd: Decimal | None = None
|
|
215
|
+
# The currency every rate on this row is stated in. The `_usd` names on
|
|
216
|
+
# rates and results predate it and are kept for existing callers.
|
|
217
|
+
currency: str = "USD"
|
|
215
218
|
|
|
216
219
|
|
|
217
220
|
@dataclass(frozen=True, slots=True)
|
|
@@ -220,7 +223,9 @@ class CostComponents:
|
|
|
220
223
|
``cost_usd`` exactly: each is quantized and the rounding residue lands in
|
|
221
224
|
the input part, so a consumer that stores them can rebuild the total
|
|
222
225
|
without a second rounding. ``request_usd`` is a per-call fee, zero on
|
|
223
|
-
every row that has none.
|
|
226
|
+
every row that has none. Each part is in the applied price's currency;
|
|
227
|
+
``input``, ``output``, ``cache_read``, ``cache_write`` and ``request``
|
|
228
|
+
name the same values without implying USD."""
|
|
224
229
|
|
|
225
230
|
input_usd: Decimal
|
|
226
231
|
output_usd: Decimal
|
|
@@ -228,6 +233,26 @@ class CostComponents:
|
|
|
228
233
|
cache_write_usd: Decimal
|
|
229
234
|
request_usd: Decimal = Decimal("0")
|
|
230
235
|
|
|
236
|
+
@property
|
|
237
|
+
def input(self) -> Decimal:
|
|
238
|
+
return self.input_usd
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def output(self) -> Decimal:
|
|
242
|
+
return self.output_usd
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def cache_read(self) -> Decimal:
|
|
246
|
+
return self.cache_read_usd
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def cache_write(self) -> Decimal:
|
|
250
|
+
return self.cache_write_usd
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def request(self) -> Decimal:
|
|
254
|
+
return self.request_usd
|
|
255
|
+
|
|
231
256
|
|
|
232
257
|
@dataclass(frozen=True, slots=True)
|
|
233
258
|
class CostResult:
|
|
@@ -241,6 +266,53 @@ class CostResult:
|
|
|
241
266
|
# The catalog's publisher for the priced model, for billing rules that
|
|
242
267
|
# apply per publisher rather than per channel.
|
|
243
268
|
publisher: str | None = None
|
|
269
|
+
# The applied price, its currency, and the time it was resolved at.
|
|
270
|
+
price: Price | None = None
|
|
271
|
+
currency: str | None = None
|
|
272
|
+
effective_at: datetime | None = None
|
|
273
|
+
# Inputs the caller did not supply that the figure depends on: "region",
|
|
274
|
+
# "channel", "input_tokens", "output_tokens".
|
|
275
|
+
missing_dimensions: tuple[str, ...] = ()
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def model_id(self) -> str | None:
|
|
279
|
+
"""The resolved catalog model id; the same value as ``canonical_model``."""
|
|
280
|
+
return self.canonical_model
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def cost(self) -> Decimal | None:
|
|
284
|
+
"""The total in ``currency``; the same value as ``cost_usd``."""
|
|
285
|
+
return self.cost_usd
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True, slots=True)
|
|
289
|
+
class ModelUsage:
|
|
290
|
+
"""Token usage for one model call. A count left ``None`` is unknown, not
|
|
291
|
+
zero: input and output turn the result partial, and cached counts bill as
|
|
292
|
+
nothing cached, as in :meth:`CatalogSnapshot.cost`."""
|
|
293
|
+
|
|
294
|
+
input_tokens: int | None = None
|
|
295
|
+
output_tokens: int | None = None
|
|
296
|
+
cache_read_tokens: int | None = None
|
|
297
|
+
cache_write_tokens: int | None = None
|
|
298
|
+
cache_write_5m_tokens: int | None = None
|
|
299
|
+
cache_write_1h_tokens: int | None = None
|
|
300
|
+
batch: bool = False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@dataclass(frozen=True, slots=True)
|
|
304
|
+
class PriceResult:
|
|
305
|
+
"""The price a model call would be billed at.
|
|
306
|
+
|
|
307
|
+
``status`` is ``priced``, ``partial`` (a price applies but depends on a
|
|
308
|
+
dimension listed in ``missing_dimensions``), or ``unpriced``. ``model_id``
|
|
309
|
+
is the resolved catalog id, not the alias the caller sent."""
|
|
310
|
+
|
|
311
|
+
status: str
|
|
312
|
+
model_id: str | None
|
|
313
|
+
price: Price | None
|
|
314
|
+
effective_at: datetime
|
|
315
|
+
missing_dimensions: tuple[str, ...] = ()
|
|
244
316
|
|
|
245
317
|
|
|
246
318
|
@dataclass(frozen=True, slots=True)
|
|
@@ -252,6 +324,26 @@ class ResolvedPrice:
|
|
|
252
324
|
rules: Mapping[str, Any]
|
|
253
325
|
|
|
254
326
|
|
|
327
|
+
def _billed_terms(resolved: ResolvedPrice) -> tuple[Any, ...]:
|
|
328
|
+
"""What a call is billed by: two resolutions with equal terms cost the
|
|
329
|
+
same for any usage, whichever region or source row they came from."""
|
|
330
|
+
price = resolved.price
|
|
331
|
+
return (
|
|
332
|
+
resolved.canonical_model,
|
|
333
|
+
price.pricing_channel,
|
|
334
|
+
price.currency,
|
|
335
|
+
price.input_per_mtok,
|
|
336
|
+
price.output_per_mtok,
|
|
337
|
+
price.cache_read_per_mtok,
|
|
338
|
+
price.cache_write_5m_per_mtok,
|
|
339
|
+
price.cache_write_1h_per_mtok,
|
|
340
|
+
price.batch_input_per_mtok,
|
|
341
|
+
price.batch_output_per_mtok,
|
|
342
|
+
price.request_fee_per_1k_usd,
|
|
343
|
+
resolved.rules,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
255
347
|
def _decimal(value: Any) -> Decimal | None:
|
|
256
348
|
if value is None:
|
|
257
349
|
return None
|
|
@@ -351,9 +443,9 @@ def _price_tokens(
|
|
|
351
443
|
batch: bool,
|
|
352
444
|
) -> tuple[Decimal, list[str], CostComponents]:
|
|
353
445
|
"""Cost a token usage against one already-selected price and its merged
|
|
354
|
-
rules.
|
|
355
|
-
|
|
356
|
-
|
|
446
|
+
rules. Every token entry point reaches it through ``_cost_result`` so all
|
|
447
|
+
compute identically. Returns the total, the reasons it is partial, and the
|
|
448
|
+
split by driver."""
|
|
357
449
|
reasons: list[str] = []
|
|
358
450
|
input_count = _tokens(input_tokens)
|
|
359
451
|
output_count = _tokens(output_tokens)
|
|
@@ -489,6 +581,48 @@ def _price_tokens(
|
|
|
489
581
|
return cost, reasons, CostComponents(*parts)
|
|
490
582
|
|
|
491
583
|
|
|
584
|
+
def _cost_result(
|
|
585
|
+
resolved: ResolvedPrice,
|
|
586
|
+
*,
|
|
587
|
+
at: datetime,
|
|
588
|
+
usage: ModelUsage,
|
|
589
|
+
missing: tuple[str, ...] = (),
|
|
590
|
+
) -> CostResult:
|
|
591
|
+
"""Cost ``usage`` against a resolved price. Every token entry point builds
|
|
592
|
+
its result here, so all of them compute and report identically."""
|
|
593
|
+
cost, reasons, components = _price_tokens(
|
|
594
|
+
resolved.price,
|
|
595
|
+
resolved.rules,
|
|
596
|
+
at=at,
|
|
597
|
+
input_tokens=usage.input_tokens,
|
|
598
|
+
output_tokens=usage.output_tokens,
|
|
599
|
+
cache_read_tokens=usage.cache_read_tokens,
|
|
600
|
+
cache_write_tokens=usage.cache_write_tokens,
|
|
601
|
+
cache_write_5m_tokens=usage.cache_write_5m_tokens,
|
|
602
|
+
cache_write_1h_tokens=usage.cache_write_1h_tokens,
|
|
603
|
+
batch=usage.batch,
|
|
604
|
+
)
|
|
605
|
+
reasons = [f"missing_{dimension}" for dimension in missing] + reasons
|
|
606
|
+
missing += tuple(
|
|
607
|
+
dimension
|
|
608
|
+
for dimension in ("input_tokens", "output_tokens")
|
|
609
|
+
if f"missing_{dimension}" in reasons
|
|
610
|
+
)
|
|
611
|
+
return CostResult(
|
|
612
|
+
resolved.canonical_model,
|
|
613
|
+
resolved.price.id,
|
|
614
|
+
cost,
|
|
615
|
+
"partial" if reasons else "priced",
|
|
616
|
+
tuple(dict.fromkeys(reasons)),
|
|
617
|
+
components=components,
|
|
618
|
+
publisher=resolved.price.publisher,
|
|
619
|
+
price=resolved.price,
|
|
620
|
+
currency=resolved.price.currency,
|
|
621
|
+
effective_at=at,
|
|
622
|
+
missing_dimensions=missing,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
492
626
|
class CatalogSnapshot:
|
|
493
627
|
def __init__(
|
|
494
628
|
self,
|
|
@@ -532,8 +666,20 @@ class CatalogSnapshot:
|
|
|
532
666
|
)
|
|
533
667
|
for candidates in self._prices.values():
|
|
534
668
|
candidates.sort(key=lambda price: price.effective_from, reverse=True)
|
|
535
|
-
|
|
536
|
-
|
|
669
|
+
# Every named region a price or pin exists for: the regions an omitted
|
|
670
|
+
# query region could have meant.
|
|
671
|
+
self._regions = tuple(sorted(
|
|
672
|
+
{price.region.lower() for price in prices}.union(
|
|
673
|
+
region
|
|
674
|
+
for candidates in self._aliases.values()
|
|
675
|
+
for alias in candidates
|
|
676
|
+
for region in alias.price_region
|
|
677
|
+
) - {"*", _GLOBAL_REGION}
|
|
678
|
+
))
|
|
679
|
+
|
|
680
|
+
def _alias_for(
|
|
681
|
+
self, candidates: tuple[Alias, ...], at: datetime, region: str | None = None
|
|
682
|
+
) -> Alias | None:
|
|
537
683
|
when = _coerce_datetime(at)
|
|
538
684
|
effective = [
|
|
539
685
|
alias
|
|
@@ -542,29 +688,31 @@ class CatalogSnapshot:
|
|
|
542
688
|
and (alias.effective_to is None or when < alias.effective_to)
|
|
543
689
|
]
|
|
544
690
|
# Regional pins constrain availability; global pins match everywhere.
|
|
545
|
-
for
|
|
691
|
+
for pinned in dict.fromkeys((region or self._region, _GLOBAL_REGION)):
|
|
546
692
|
for alias in effective:
|
|
547
|
-
if
|
|
693
|
+
if pinned in alias.price_region:
|
|
548
694
|
return alias
|
|
549
695
|
for alias in effective:
|
|
550
696
|
if not alias.price_region:
|
|
551
697
|
return alias
|
|
552
698
|
return None
|
|
553
699
|
|
|
554
|
-
def _price_regions(self, alias: Alias) -> tuple[str, ...]:
|
|
700
|
+
def _price_regions(self, alias: Alias, region: str) -> tuple[str, ...]:
|
|
555
701
|
if not alias.price_region:
|
|
556
|
-
return tuple(dict.fromkeys((
|
|
557
|
-
for
|
|
558
|
-
if
|
|
559
|
-
return (
|
|
702
|
+
return tuple(dict.fromkeys((region, "*", _GLOBAL_REGION)))
|
|
703
|
+
for candidate in (region, _GLOBAL_REGION):
|
|
704
|
+
if candidate in alias.price_region:
|
|
705
|
+
return (candidate,)
|
|
560
706
|
return ()
|
|
561
707
|
|
|
562
|
-
def _price_for(
|
|
708
|
+
def _price_for(
|
|
709
|
+
self, alias: Alias, at: datetime, region: str | None = None
|
|
710
|
+
) -> Price | None:
|
|
563
711
|
at = at if at.tzinfo else at.replace(tzinfo=timezone.utc)
|
|
564
712
|
candidates = self._prices.get((alias.model_id, alias.pricing_channel), [])
|
|
565
|
-
for
|
|
713
|
+
for scope in self._price_regions(alias, region or self._region):
|
|
566
714
|
for price in candidates:
|
|
567
|
-
if price.region.lower() !=
|
|
715
|
+
if price.region.lower() != scope:
|
|
568
716
|
continue
|
|
569
717
|
if price.effective_from <= at and (
|
|
570
718
|
price.effective_to is None or at < price.effective_to
|
|
@@ -611,25 +759,31 @@ class CatalogSnapshot:
|
|
|
611
759
|
deployment channel before making a provider call. It never falls back
|
|
612
760
|
to pricing from a different channel.
|
|
613
761
|
"""
|
|
762
|
+
return self._resolve(model=model, channel=channel, at=at)
|
|
614
763
|
|
|
764
|
+
def _resolve(
|
|
765
|
+
self, *, model: Any, channel: Any, at: Any, region: str | None = None
|
|
766
|
+
) -> ResolvedPrice | None:
|
|
767
|
+
"""``resolve_price`` in ``region``, or the snapshot's region when
|
|
768
|
+
``None``."""
|
|
615
769
|
model_key = str(model or "").strip().lower()
|
|
616
770
|
channel_key = str(channel or "").strip().lower()
|
|
617
771
|
when = _coerce_datetime(at)
|
|
618
772
|
candidates = self._deployment_aliases.get((model_key, channel_key), ())
|
|
619
|
-
alias = self._alias_for(candidates, when)
|
|
773
|
+
alias = self._alias_for(candidates, when, region)
|
|
620
774
|
if alias is None and channel_key in _BEDROCK_CHANNELS:
|
|
621
775
|
for candidate, dropped_global in _bedrock_fallback_ids(model_key):
|
|
622
776
|
if dropped_global and channel_key != _BEDROCK_CHANNEL:
|
|
623
777
|
continue
|
|
624
778
|
candidates = self._deployment_aliases.get((candidate, channel_key), ())
|
|
625
|
-
alias = self._alias_for(candidates, when)
|
|
779
|
+
alias = self._alias_for(candidates, when, region)
|
|
626
780
|
if alias is not None and dropped_global and _crosses_price_region(alias):
|
|
627
781
|
alias = None
|
|
628
782
|
if alias is not None:
|
|
629
783
|
break
|
|
630
784
|
if alias is None:
|
|
631
785
|
return None
|
|
632
|
-
price = self._price_for(alias, when)
|
|
786
|
+
price = self._price_for(alias, when, region)
|
|
633
787
|
if price is None:
|
|
634
788
|
return None
|
|
635
789
|
return ResolvedPrice(
|
|
@@ -638,6 +792,41 @@ class CatalogSnapshot:
|
|
|
638
792
|
rules=MappingProxyType({**price.rules, **alias.rules}),
|
|
639
793
|
)
|
|
640
794
|
|
|
795
|
+
def _lookup(
|
|
796
|
+
self, *, model: Any, channel: Any, provider: Any, at: Any, region: Any
|
|
797
|
+
) -> tuple[ResolvedPrice | None, tuple[str, ...]]:
|
|
798
|
+
"""Resolve a model call for the public lookup and cost entry points,
|
|
799
|
+
with the dimensions the answer depends on but the caller left out.
|
|
800
|
+
|
|
801
|
+
An explicit channel wins; otherwise a provider names its direct
|
|
802
|
+
channel. An explicit region resolves that region. An omitted one uses
|
|
803
|
+
the snapshot's region, and when that is the default ``global`` it is
|
|
804
|
+
checked against every named region: a global price that some region
|
|
805
|
+
bills differently is returned with ``region`` missing, and a model only
|
|
806
|
+
some region prices is unpriced with ``region`` missing.
|
|
807
|
+
"""
|
|
808
|
+
if channel is None or not str(channel).strip():
|
|
809
|
+
channel = direct_channel_for_provider(provider)
|
|
810
|
+
if channel is None:
|
|
811
|
+
return None, ("channel",)
|
|
812
|
+
stated = str(region or "").strip().lower() or None
|
|
813
|
+
resolved = self._resolve(model=model, channel=channel, at=at, region=stated)
|
|
814
|
+
if stated is not None or self._region != _GLOBAL_REGION:
|
|
815
|
+
return resolved, ()
|
|
816
|
+
regional = [
|
|
817
|
+
found
|
|
818
|
+
for found in (
|
|
819
|
+
self._resolve(model=model, channel=channel, at=at, region=name)
|
|
820
|
+
for name in self._regions
|
|
821
|
+
)
|
|
822
|
+
if found is not None
|
|
823
|
+
]
|
|
824
|
+
if resolved is None:
|
|
825
|
+
return None, ("region",) if regional else ()
|
|
826
|
+
if any(_billed_terms(found) != _billed_terms(resolved) for found in regional):
|
|
827
|
+
return resolved, ("region",)
|
|
828
|
+
return resolved, ()
|
|
829
|
+
|
|
641
830
|
def cost(
|
|
642
831
|
self,
|
|
643
832
|
*,
|
|
@@ -688,27 +877,18 @@ class CatalogSnapshot:
|
|
|
688
877
|
"unpriced",
|
|
689
878
|
("no_effective_price",),
|
|
690
879
|
)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
price,
|
|
694
|
-
{**price.rules, **alias.rules},
|
|
880
|
+
return _cost_result(
|
|
881
|
+
ResolvedPrice(alias.canonical_id, price, {**price.rules, **alias.rules}),
|
|
695
882
|
at=when,
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
alias.canonical_id,
|
|
706
|
-
price.id,
|
|
707
|
-
cost,
|
|
708
|
-
"partial" if reasons else "priced",
|
|
709
|
-
tuple(dict.fromkeys(reasons)),
|
|
710
|
-
components=components,
|
|
711
|
-
publisher=price.publisher,
|
|
883
|
+
usage=ModelUsage(
|
|
884
|
+
input_tokens=input_tokens,
|
|
885
|
+
output_tokens=output_tokens,
|
|
886
|
+
cache_read_tokens=cache_read_tokens,
|
|
887
|
+
cache_write_tokens=cache_write_tokens,
|
|
888
|
+
cache_write_5m_tokens=cache_write_5m_tokens,
|
|
889
|
+
cache_write_1h_tokens=cache_write_1h_tokens,
|
|
890
|
+
batch=batch,
|
|
891
|
+
),
|
|
712
892
|
)
|
|
713
893
|
|
|
714
894
|
def price_deployment(
|
|
@@ -739,24 +919,16 @@ class CatalogSnapshot:
|
|
|
739
919
|
resolved = self.resolve_price(model=model, channel=channel, at=when)
|
|
740
920
|
if resolved is None:
|
|
741
921
|
return CostResult(None, None, None, "unpriced", ("unknown_deployment",))
|
|
742
|
-
|
|
743
|
-
resolved
|
|
744
|
-
resolved.rules,
|
|
922
|
+
return _cost_result(
|
|
923
|
+
resolved,
|
|
745
924
|
at=when,
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
resolved.canonical_model,
|
|
756
|
-
resolved.price.id,
|
|
757
|
-
cost,
|
|
758
|
-
"partial" if reasons else "priced",
|
|
759
|
-
tuple(dict.fromkeys(reasons)),
|
|
760
|
-
components=components,
|
|
761
|
-
publisher=resolved.price.publisher,
|
|
925
|
+
usage=ModelUsage(
|
|
926
|
+
input_tokens=input_tokens,
|
|
927
|
+
output_tokens=output_tokens,
|
|
928
|
+
cache_read_tokens=cache_read_tokens,
|
|
929
|
+
cache_write_tokens=cache_write_tokens,
|
|
930
|
+
cache_write_5m_tokens=cache_write_5m_tokens,
|
|
931
|
+
cache_write_1h_tokens=cache_write_1h_tokens,
|
|
932
|
+
batch=batch,
|
|
933
|
+
),
|
|
762
934
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
version: '2026-10-
|
|
1
|
+
version: '2026-10-19'
|
|
2
2
|
models:
|
|
3
3
|
- canonical_id: openai/gpt-5.6-sol
|
|
4
4
|
display_name: GPT-5.6 Sol
|
|
@@ -447,6 +447,30 @@ models:
|
|
|
447
447
|
pricing_channel: vercel-ai-gateway
|
|
448
448
|
execution_profiles:
|
|
449
449
|
- default
|
|
450
|
+
- canonical_id: deepseek/deepseek-v4.1-flash-1017
|
|
451
|
+
display_name: DeepSeek V4.1 Flash 1017
|
|
452
|
+
publisher: deepseek
|
|
453
|
+
released: '2026-10-17' # https://vercel.com/ai-gateway/models/deepseek-v4.1-flash-1017
|
|
454
|
+
routes:
|
|
455
|
+
- key: default:deepseek/deepseek-v4.1-flash-1017
|
|
456
|
+
id: deepseek/deepseek-v4.1-flash-1017
|
|
457
|
+
provider: vercel
|
|
458
|
+
model_id: deepseek/deepseek-v4.1-flash-1017
|
|
459
|
+
pricing_channel: vercel-ai-gateway
|
|
460
|
+
execution_profiles:
|
|
461
|
+
- default
|
|
462
|
+
- canonical_id: deepseek/deepseek-v4.1-flash-1017-fast
|
|
463
|
+
display_name: DeepSeek V4.1 Flash 1017 Fast
|
|
464
|
+
publisher: deepseek
|
|
465
|
+
released: '2026-10-17' # https://vercel.com/ai-gateway/models/deepseek-v4.1-flash-1017-fast
|
|
466
|
+
routes:
|
|
467
|
+
- key: default:deepseek/deepseek-v4.1-flash-1017-fast
|
|
468
|
+
id: deepseek/deepseek-v4.1-flash-1017-fast
|
|
469
|
+
provider: vercel
|
|
470
|
+
model_id: deepseek/deepseek-v4.1-flash-1017-fast
|
|
471
|
+
pricing_channel: vercel-ai-gateway
|
|
472
|
+
execution_profiles:
|
|
473
|
+
- default
|
|
450
474
|
- canonical_id: alibaba/qwen3.8-flash
|
|
451
475
|
display_name: Qwen 3.8 Flash
|
|
452
476
|
publisher: alibaba
|
|
@@ -471,6 +495,18 @@ models:
|
|
|
471
495
|
pricing_channel: vercel-ai-gateway
|
|
472
496
|
execution_profiles:
|
|
473
497
|
- default
|
|
498
|
+
- canonical_id: inception/mercury-3
|
|
499
|
+
display_name: Mercury 3
|
|
500
|
+
publisher: inception
|
|
501
|
+
released: '2026-10-19' # https://www.inceptionlabs.ai/blog/introducing-mercury-3
|
|
502
|
+
routes:
|
|
503
|
+
- key: default:inception/mercury-3
|
|
504
|
+
id: inception/mercury-3
|
|
505
|
+
provider: vercel
|
|
506
|
+
model_id: inception/mercury-3
|
|
507
|
+
pricing_channel: vercel-ai-gateway
|
|
508
|
+
execution_profiles:
|
|
509
|
+
- default
|
|
474
510
|
- canonical_id: openai/gpt-5.4-mini
|
|
475
511
|
display_name: GPT-5.4 Mini
|
|
476
512
|
publisher: openai
|
|
@@ -882,6 +918,9 @@ execution_profiles:
|
|
|
882
918
|
- default:meta/muse-wonder-1.0
|
|
883
919
|
- default:meta/muse-spark-open
|
|
884
920
|
- default:nvidia/nemotron-3.5-super-vl
|
|
921
|
+
- default:deepseek/deepseek-v4.1-flash-1017
|
|
922
|
+
- default:deepseek/deepseek-v4.1-flash-1017-fast
|
|
923
|
+
- default:inception/mercury-3
|
|
885
924
|
- default:openai:gpt-6.1-sol
|
|
886
925
|
- default:anthropic:claude-sonnet-5-5
|
|
887
926
|
- default:anthropic:claude-haiku-5-5
|
|
@@ -948,6 +987,9 @@ offer_groups:
|
|
|
948
987
|
- default:meta/muse-wonder-1.0
|
|
949
988
|
- default:meta/muse-spark-open
|
|
950
989
|
- default:nvidia/nemotron-3.5-super-vl
|
|
990
|
+
- default:deepseek/deepseek-v4.1-flash-1017
|
|
991
|
+
- default:deepseek/deepseek-v4.1-flash-1017-fast
|
|
992
|
+
- default:inception/mercury-3
|
|
951
993
|
- id: fireworks
|
|
952
994
|
credential: FIREWORKS_API_KEY
|
|
953
995
|
routes:
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Community updates welcome: add an alias or price entry with its provider
|
|
8
8
|
# source_url and open a PR. CI validates structure and date overlaps.
|
|
9
|
-
version: "2026-10-
|
|
9
|
+
version: "2026-10-19"
|
|
10
10
|
currency: USD
|
|
11
|
-
pricing_verified_at: "2026-10-
|
|
11
|
+
pricing_verified_at: "2026-10-19"
|
|
12
12
|
models:
|
|
13
13
|
- canonical_id: openai/gpt-6-sol
|
|
14
14
|
publisher: openai
|
|
@@ -2709,12 +2709,28 @@ models:
|
|
|
2709
2709
|
- channel: vercel-ai-gateway
|
|
2710
2710
|
region: global
|
|
2711
2711
|
effective_from: "2026-10-02"
|
|
2712
|
+
effective_to: "2026-10-19"
|
|
2712
2713
|
input_per_mtok: 0.30
|
|
2713
2714
|
output_per_mtok: 1.20
|
|
2714
2715
|
cache_read_per_mtok: 0.007
|
|
2715
2716
|
rules:
|
|
2716
2717
|
varies_by_provider: true
|
|
2717
2718
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
2719
|
+
- channel: vercel-ai-gateway
|
|
2720
|
+
region: global
|
|
2721
|
+
effective_from: "2026-10-19"
|
|
2722
|
+
# Store Vercel's 2x peak rate; this rule restores its off-peak rate.
|
|
2723
|
+
input_per_mtok: 0.15
|
|
2724
|
+
output_per_mtok: 0.60
|
|
2725
|
+
cache_read_per_mtok: 0.003
|
|
2726
|
+
rules:
|
|
2727
|
+
input_includes_cache_read: true
|
|
2728
|
+
varies_by_provider: true
|
|
2729
|
+
off_peak_discount:
|
|
2730
|
+
multiplier: 0.5
|
|
2731
|
+
peak_hours_utc: [[1, 4], [6, 10]]
|
|
2732
|
+
peak_weekdays_only: true
|
|
2733
|
+
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
2718
2734
|
# Displayed serverless billing rates take precedence over conflicting prose.
|
|
2719
2735
|
- channel: fireworks-api
|
|
2720
2736
|
region: global
|
|
@@ -4602,6 +4618,119 @@ models:
|
|
|
4602
4618
|
input_per_mtok: 0.00
|
|
4603
4619
|
output_per_mtok: 0.00
|
|
4604
4620
|
source_url: https://vercel.com/ai-gateway/models/nemotron-3.5-lightning-free
|
|
4621
|
+
|
|
4622
|
+
- canonical_id: deepseek/deepseek-v4.1-flash-1017
|
|
4623
|
+
publisher: deepseek
|
|
4624
|
+
aliases:
|
|
4625
|
+
- provider: deepseek
|
|
4626
|
+
alias: deepseek/deepseek-v4.1-flash-1017
|
|
4627
|
+
channel: vercel-ai-gateway
|
|
4628
|
+
rules: {input_includes_cache_read: true}
|
|
4629
|
+
prices:
|
|
4630
|
+
- channel: vercel-ai-gateway
|
|
4631
|
+
region: global
|
|
4632
|
+
effective_from: "2026-10-17"
|
|
4633
|
+
# Store Vercel's 2x peak rate; this rule restores its off-peak rate.
|
|
4634
|
+
input_per_mtok: 0.15
|
|
4635
|
+
output_per_mtok: 0.60
|
|
4636
|
+
cache_read_per_mtok: 0.003
|
|
4637
|
+
rules:
|
|
4638
|
+
input_includes_cache_read: true
|
|
4639
|
+
varies_by_provider: true
|
|
4640
|
+
off_peak_discount:
|
|
4641
|
+
multiplier: 0.5
|
|
4642
|
+
peak_hours_utc: [[1, 4], [6, 10]]
|
|
4643
|
+
peak_weekdays_only: true
|
|
4644
|
+
source_url: https://vercel.com/ai-gateway/models/deepseek-v4.1-flash-1017
|
|
4645
|
+
|
|
4646
|
+
- canonical_id: deepseek/deepseek-v4.1-flash-1017-fast
|
|
4647
|
+
publisher: deepseek
|
|
4648
|
+
aliases:
|
|
4649
|
+
- provider: deepseek
|
|
4650
|
+
alias: deepseek/deepseek-v4.1-flash-1017-fast
|
|
4651
|
+
channel: vercel-ai-gateway
|
|
4652
|
+
rules: {input_includes_cache_read: true}
|
|
4653
|
+
prices:
|
|
4654
|
+
- channel: vercel-ai-gateway
|
|
4655
|
+
region: global
|
|
4656
|
+
effective_from: "2026-10-17"
|
|
4657
|
+
# The source lists a fixed Baseten rate without peak pricing.
|
|
4658
|
+
input_per_mtok: 0.60
|
|
4659
|
+
output_per_mtok: 2.40
|
|
4660
|
+
cache_read_per_mtok: 0.014
|
|
4661
|
+
source_url: https://vercel.com/ai-gateway/models/deepseek-v4.1-flash-1017-fast
|
|
4662
|
+
|
|
4663
|
+
# Price observed traffic, but exclude this route from default execution while
|
|
4664
|
+
# Vercel permits retention and prompt training.
|
|
4665
|
+
- canonical_id: elorian/glimpse
|
|
4666
|
+
publisher: elorian
|
|
4667
|
+
aliases:
|
|
4668
|
+
- provider: elorian
|
|
4669
|
+
alias: elorian/glimpse
|
|
4670
|
+
channel: vercel-ai-gateway
|
|
4671
|
+
rules: {input_includes_cache_read: true}
|
|
4672
|
+
prices:
|
|
4673
|
+
- channel: vercel-ai-gateway
|
|
4674
|
+
region: global
|
|
4675
|
+
effective_from: "2026-10-16"
|
|
4676
|
+
input_per_mtok: 0.60
|
|
4677
|
+
output_per_mtok: 3.00
|
|
4678
|
+
cache_read_per_mtok: 0.06
|
|
4679
|
+
source_url: https://vercel.com/ai-gateway/models/glimpse
|
|
4680
|
+
|
|
4681
|
+
- canonical_id: inception/mercury-3
|
|
4682
|
+
publisher: inception
|
|
4683
|
+
aliases:
|
|
4684
|
+
- provider: inception
|
|
4685
|
+
alias: inception/mercury-3
|
|
4686
|
+
channel: vercel-ai-gateway
|
|
4687
|
+
rules: {input_includes_cache_read: true}
|
|
4688
|
+
prices:
|
|
4689
|
+
# No end date is published for this 50% launch price. Close the row when
|
|
4690
|
+
# Inception or Vercel announces the standard rate.
|
|
4691
|
+
- channel: vercel-ai-gateway
|
|
4692
|
+
region: global
|
|
4693
|
+
effective_from: "2026-10-19"
|
|
4694
|
+
input_per_mtok: 0.10
|
|
4695
|
+
output_per_mtok: 0.35
|
|
4696
|
+
cache_read_per_mtok: 0.01
|
|
4697
|
+
source_url: https://www.inceptionlabs.ai/blog/introducing-mercury-3
|
|
4698
|
+
|
|
4699
|
+
# Price observed traffic, but exclude this route from default execution while
|
|
4700
|
+
# Vercel permits retention and prompt training.
|
|
4701
|
+
- canonical_id: minimax/minimax-m3.1-flash
|
|
4702
|
+
publisher: minimax
|
|
4703
|
+
aliases:
|
|
4704
|
+
- {provider: minimax, alias: minimax-m3.1-flash, channel: minimax-api}
|
|
4705
|
+
- {provider: litellm, alias: minimax-m3.1-flash, channel: minimax-api}
|
|
4706
|
+
- {provider: unknown, alias: minimax-m3.1-flash, channel: minimax-api}
|
|
4707
|
+
- provider: minimax
|
|
4708
|
+
alias: minimax/minimax-m3.1-flash
|
|
4709
|
+
channel: vercel-ai-gateway
|
|
4710
|
+
rules: {input_includes_cache_read: true}
|
|
4711
|
+
prices:
|
|
4712
|
+
- channel: minimax-api
|
|
4713
|
+
region: global
|
|
4714
|
+
effective_from: "2026-10-19"
|
|
4715
|
+
input_per_mtok: 0.15
|
|
4716
|
+
output_per_mtok: 0.60
|
|
4717
|
+
cache_read_per_mtok: 0.015
|
|
4718
|
+
source_url: https://www.minimax.io/blog/minimax-m31f
|
|
4719
|
+
- channel: vercel-ai-gateway
|
|
4720
|
+
region: global
|
|
4721
|
+
effective_from: "2026-10-19"
|
|
4722
|
+
effective_to: "2026-10-23"
|
|
4723
|
+
input_per_mtok: 0.00
|
|
4724
|
+
output_per_mtok: 0.00
|
|
4725
|
+
cache_read_per_mtok: 0.00
|
|
4726
|
+
source_url: https://vercel.com/changelog/minimax-m3-1-flash-now-available-on-ai-gateway-for-free
|
|
4727
|
+
- channel: vercel-ai-gateway
|
|
4728
|
+
region: global
|
|
4729
|
+
effective_from: "2026-10-23"
|
|
4730
|
+
input_per_mtok: 0.15
|
|
4731
|
+
output_per_mtok: 0.60
|
|
4732
|
+
cache_read_per_mtok: 0.015
|
|
4733
|
+
source_url: https://vercel.com/changelog/minimax-m3-1-flash-now-available-on-ai-gateway-for-free
|
|
4605
4734
|
retrieval:
|
|
4606
4735
|
- channel: anthropic-api
|
|
4607
4736
|
operation: web_search
|
|
@@ -13,7 +13,11 @@ from .catalog import (
|
|
|
13
13
|
Alias,
|
|
14
14
|
CatalogSnapshot,
|
|
15
15
|
CostResult,
|
|
16
|
+
ModelUsage,
|
|
16
17
|
Price,
|
|
18
|
+
PriceResult,
|
|
19
|
+
_coerce_datetime,
|
|
20
|
+
_cost_result,
|
|
17
21
|
_decimal,
|
|
18
22
|
_effective_windows_overlap,
|
|
19
23
|
normalize_provider,
|
|
@@ -123,6 +127,65 @@ class LoadedCatalog:
|
|
|
123
127
|
batch=batch,
|
|
124
128
|
)
|
|
125
129
|
|
|
130
|
+
def lookup_price(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
model: Any,
|
|
134
|
+
at: Any,
|
|
135
|
+
channel: Any = None,
|
|
136
|
+
provider: Any = None,
|
|
137
|
+
region: Any = None,
|
|
138
|
+
) -> PriceResult:
|
|
139
|
+
"""The price a call to ``model`` at ``at`` is billed at.
|
|
140
|
+
|
|
141
|
+
``channel`` is authoritative; without one, ``provider`` supplies its
|
|
142
|
+
direct channel. ``region`` overrides the region the catalog was
|
|
143
|
+
loaded with. A result that depends on a dimension the caller left out
|
|
144
|
+
lists it in ``missing_dimensions``."""
|
|
145
|
+
when = _coerce_datetime(at)
|
|
146
|
+
resolved, missing = self.snapshot._lookup(
|
|
147
|
+
model=model, channel=channel, provider=provider, at=when, region=region
|
|
148
|
+
)
|
|
149
|
+
if resolved is None:
|
|
150
|
+
return PriceResult("unpriced", None, None, when, missing)
|
|
151
|
+
return PriceResult(
|
|
152
|
+
"partial" if missing else "priced",
|
|
153
|
+
resolved.canonical_model,
|
|
154
|
+
resolved.price,
|
|
155
|
+
when,
|
|
156
|
+
missing,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def calculate_cost(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
model: Any,
|
|
163
|
+
usage: ModelUsage,
|
|
164
|
+
at: Any,
|
|
165
|
+
channel: Any = None,
|
|
166
|
+
provider: Any = None,
|
|
167
|
+
region: Any = None,
|
|
168
|
+
) -> CostResult:
|
|
169
|
+
"""Cost ``usage`` at the price :meth:`lookup_price` resolves. A
|
|
170
|
+
missing dimension, from the lookup or the usage, turns the result
|
|
171
|
+
``partial`` and keeps the subtotal that is known."""
|
|
172
|
+
when = _coerce_datetime(at)
|
|
173
|
+
resolved, missing = self.snapshot._lookup(
|
|
174
|
+
model=model, channel=channel, provider=provider, at=when, region=region
|
|
175
|
+
)
|
|
176
|
+
if resolved is None:
|
|
177
|
+
reasons = tuple(f"missing_{dimension}" for dimension in missing)
|
|
178
|
+
return CostResult(
|
|
179
|
+
None,
|
|
180
|
+
None,
|
|
181
|
+
None,
|
|
182
|
+
"unpriced",
|
|
183
|
+
reasons or ("unknown_deployment",),
|
|
184
|
+
effective_at=when,
|
|
185
|
+
missing_dimensions=missing,
|
|
186
|
+
)
|
|
187
|
+
return _cost_result(resolved, at=when, usage=usage, missing=missing)
|
|
188
|
+
|
|
126
189
|
def price_retrieval(
|
|
127
190
|
self,
|
|
128
191
|
*,
|
|
@@ -191,7 +254,7 @@ def parse_catalog(
|
|
|
191
254
|
version = str(document.get("version") or "")
|
|
192
255
|
if not version:
|
|
193
256
|
raise CatalogError("prices document must have a version")
|
|
194
|
-
_catalog_metadata(document)
|
|
257
|
+
currency, _ = _catalog_metadata(document)
|
|
195
258
|
aliases: dict[tuple[str, str], list[Alias]] = {}
|
|
196
259
|
prices: list[Price] = []
|
|
197
260
|
for entry in document["models"]:
|
|
@@ -311,6 +374,7 @@ def parse_catalog(
|
|
|
311
374
|
effective_to=effective_to,
|
|
312
375
|
source_url=str(price["source_url"]).strip(),
|
|
313
376
|
publisher=publisher,
|
|
377
|
+
currency=currency,
|
|
314
378
|
)
|
|
315
379
|
)
|
|
316
380
|
return version, {key: tuple(value) for key, value in aliases.items()}, prices
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metergraph-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Reusable MeterGraph catalog and deterministic billing engine
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -92,6 +92,58 @@ prefix, the latter only onto an `aws-bedrock` alias. Geographic prefixes such as
|
|
|
92
92
|
`LoadedCatalog.pricing_verified_at` records when the bundled catalog was last
|
|
93
93
|
checked against its linked provider sources.
|
|
94
94
|
|
|
95
|
+
### Direct lookup and usage cost
|
|
96
|
+
|
|
97
|
+
`lookup_price` returns the price a call is billed at, and `calculate_cost`
|
|
98
|
+
costs a call's usage at that price:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from metergraph_core import ModelUsage
|
|
102
|
+
|
|
103
|
+
found = catalog.lookup_price(
|
|
104
|
+
model="gpt-5.4-mini",
|
|
105
|
+
provider="openai",
|
|
106
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
107
|
+
)
|
|
108
|
+
print(found.status, found.model_id, found.price.id, found.price.currency)
|
|
109
|
+
|
|
110
|
+
result = catalog.calculate_cost(
|
|
111
|
+
model="gpt-5.4-mini",
|
|
112
|
+
provider="openai",
|
|
113
|
+
at=datetime(2026, 8, 17, tzinfo=timezone.utc),
|
|
114
|
+
usage=ModelUsage(input_tokens=1000, output_tokens=200, cache_read_tokens=500),
|
|
115
|
+
)
|
|
116
|
+
print(result.status, result.model_id, result.cost, result.currency)
|
|
117
|
+
print(result.components.input, result.components.cache_read)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Both resolve exactly as `resolve_price` and `price` do, including
|
|
121
|
+
effective-date windows and the Bedrock fallback, and cost through the same
|
|
122
|
+
arithmetic. `model_id` is the resolved catalog ID, not the alias passed in, and
|
|
123
|
+
`price` is the applied catalog row. An explicit `channel` is authoritative;
|
|
124
|
+
without one, `provider` supplies its direct channel, and with neither the
|
|
125
|
+
result is `unpriced` with `missing_dimensions=("channel",)`.
|
|
126
|
+
|
|
127
|
+
Every `Price` states its `currency`, and `CostResult.currency` is the currency
|
|
128
|
+
of the applied price. The bundled catalog is USD. `cost` and
|
|
129
|
+
`components.input`, `output`, `cache_read`, `cache_write` and `request` are in
|
|
130
|
+
that currency; the older `cost_usd` and `*_usd` names return the same values.
|
|
131
|
+
|
|
132
|
+
A `ModelUsage` count left `None` is unknown, not zero. A missing input or
|
|
133
|
+
output count keeps the subtotal that is known, marks the result `partial`, and
|
|
134
|
+
lists `input_tokens` or `output_tokens` in `missing_dimensions`.
|
|
135
|
+
|
|
136
|
+
`region` selects a regional price and overrides the `region` passed to
|
|
137
|
+
`load_catalog`. When neither names a region:
|
|
138
|
+
|
|
139
|
+
- if every region bills the model the same, the result is `priced`;
|
|
140
|
+
- if a global price applies but some region bills differently, the global
|
|
141
|
+
price is returned as `partial` with `missing_dimensions=("region",)`;
|
|
142
|
+
- if only specific regions price the model, the result is `unpriced` with
|
|
143
|
+
`missing_dimensions=("region",)`.
|
|
144
|
+
|
|
145
|
+
A region the model is not sold in is `unpriced` with nothing missing.
|
|
146
|
+
|
|
95
147
|
## Model registry
|
|
96
148
|
|
|
97
149
|
```python
|
|
@@ -165,7 +217,9 @@ from metergraph_core import (
|
|
|
165
217
|
LoadedCatalog,
|
|
166
218
|
ModelRegistry,
|
|
167
219
|
ModelRoute,
|
|
220
|
+
ModelUsage,
|
|
168
221
|
Price,
|
|
222
|
+
PriceResult,
|
|
169
223
|
ResolvedPrice,
|
|
170
224
|
load_catalog,
|
|
171
225
|
load_model_registry,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.61 → metergraph_core-0.3.0}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|