metergraph-core 0.2.1__tar.gz → 0.2.2__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.1/src/metergraph_core.egg-info → metergraph_core-0.2.2}/PKG-INFO +1 -1
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/pyproject.toml +1 -1
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core/__init__.py +10 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core/data/prices.yaml +38 -2
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core/loader.py +96 -0
- metergraph_core-0.2.2/src/metergraph_core/retrieval.py +141 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2/src/metergraph_core.egg-info}/PKG-INFO +1 -1
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core.egg-info/SOURCES.txt +1 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/MANIFEST.in +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/README.md +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/setup.cfg +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core/billing.py +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core/catalog.py +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core.egg-info/top_level.txt +0 -0
|
@@ -19,6 +19,12 @@ from .loader import (
|
|
|
19
19
|
LoadedCatalog,
|
|
20
20
|
load_catalog,
|
|
21
21
|
parse_catalog,
|
|
22
|
+
parse_retrieval,
|
|
23
|
+
)
|
|
24
|
+
from .retrieval import (
|
|
25
|
+
RetrievalCatalog,
|
|
26
|
+
RetrievalCostResult,
|
|
27
|
+
RetrievalPrice,
|
|
22
28
|
)
|
|
23
29
|
|
|
24
30
|
__all__ = [
|
|
@@ -31,9 +37,13 @@ __all__ = [
|
|
|
31
37
|
"GatewayBillingEvidence",
|
|
32
38
|
"Price",
|
|
33
39
|
"ResolvedPrice",
|
|
40
|
+
"RetrievalCatalog",
|
|
41
|
+
"RetrievalCostResult",
|
|
42
|
+
"RetrievalPrice",
|
|
34
43
|
"direct_channel_for_provider",
|
|
35
44
|
"load_catalog",
|
|
36
45
|
"normalize_gateway_evidence",
|
|
37
46
|
"parse_catalog",
|
|
47
|
+
"parse_retrieval",
|
|
38
48
|
"resolve_billing",
|
|
39
49
|
]
|
|
@@ -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-08-
|
|
9
|
+
version: "2026-08-26"
|
|
10
10
|
currency: USD
|
|
11
|
-
pricing_verified_at: "2026-08-
|
|
11
|
+
pricing_verified_at: "2026-08-26"
|
|
12
12
|
models:
|
|
13
13
|
- canonical_id: openai/gpt-5.6-sol
|
|
14
14
|
publisher: openai
|
|
@@ -975,3 +975,39 @@ models:
|
|
|
975
975
|
output_per_mtok: 0.50
|
|
976
976
|
cache_read_per_mtok: 0.05
|
|
977
977
|
source_url: https://vercel.com/ai-gateway/models/grok-4.1-fast-reasoning/providers
|
|
978
|
+
|
|
979
|
+
# Retrieval prices: USD per 1,000 counted operations (completed searches, tool
|
|
980
|
+
# calls, executed grounding queries), billed on the provider's direct channel.
|
|
981
|
+
# These sit alongside the model-token catalog above but are priced per unit, not
|
|
982
|
+
# per token. Effective-dated like model prices: keep superseded entries and close
|
|
983
|
+
# them with `effective_to` rather than editing in place. A $0 fee is an explicit
|
|
984
|
+
# priced zero, not the absence of a price.
|
|
985
|
+
retrieval:
|
|
986
|
+
- channel: anthropic-api
|
|
987
|
+
operation: web_search
|
|
988
|
+
region: global
|
|
989
|
+
unit: completed_search
|
|
990
|
+
per_1k_usd: 10.00
|
|
991
|
+
effective_from: "2026-08-26"
|
|
992
|
+
source_url: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
|
|
993
|
+
- channel: anthropic-api
|
|
994
|
+
operation: web_fetch
|
|
995
|
+
region: global
|
|
996
|
+
unit: tool_call
|
|
997
|
+
per_1k_usd: 0.00
|
|
998
|
+
effective_from: "2026-08-26"
|
|
999
|
+
source_url: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-fetch-tool
|
|
1000
|
+
- channel: openai-api
|
|
1001
|
+
operation: web_search
|
|
1002
|
+
region: global
|
|
1003
|
+
unit: tool_call
|
|
1004
|
+
per_1k_usd: 10.00
|
|
1005
|
+
effective_from: "2026-08-26"
|
|
1006
|
+
source_url: https://platform.openai.com/docs/pricing
|
|
1007
|
+
- channel: google-api
|
|
1008
|
+
operation: google_search_grounding
|
|
1009
|
+
region: global
|
|
1010
|
+
unit: executed_query
|
|
1011
|
+
per_1k_usd: 14.00
|
|
1012
|
+
effective_from: "2026-08-26"
|
|
1013
|
+
source_url: https://ai.google.dev/gemini-api/docs/pricing
|
|
@@ -17,6 +17,7 @@ from .catalog import (
|
|
|
17
17
|
_decimal,
|
|
18
18
|
_normalize_provider,
|
|
19
19
|
)
|
|
20
|
+
from .retrieval import RetrievalCatalog, RetrievalCostResult, RetrievalPrice
|
|
20
21
|
|
|
21
22
|
DEFAULT_CATALOG_PATH = Path(__file__).parent / "data" / "prices.yaml"
|
|
22
23
|
_PROVIDER_SYNONYMS = {"bedrock": ("aws-bedrock", "aws")}
|
|
@@ -72,6 +73,7 @@ class LoadedCatalog:
|
|
|
72
73
|
document: dict[str, Any]
|
|
73
74
|
snapshot: CatalogSnapshot
|
|
74
75
|
canonical_ids: Mapping[tuple[str, str], str]
|
|
76
|
+
retrieval: RetrievalCatalog
|
|
75
77
|
|
|
76
78
|
def canonical_model_id(self, provider: str, model_id: str) -> str:
|
|
77
79
|
"""Resolve a captured provider-native model id to its canonical id,
|
|
@@ -113,6 +115,26 @@ class LoadedCatalog:
|
|
|
113
115
|
batch=batch,
|
|
114
116
|
)
|
|
115
117
|
|
|
118
|
+
def price_retrieval(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
channel: Any,
|
|
122
|
+
operation: Any,
|
|
123
|
+
units: Any,
|
|
124
|
+
at: Any,
|
|
125
|
+
region: Any = "global",
|
|
126
|
+
) -> RetrievalCostResult:
|
|
127
|
+
"""Price a retrieval operation (searches, tool calls, grounded queries)
|
|
128
|
+
by counted unit. See :meth:`RetrievalCatalog.price`. This is kept
|
|
129
|
+
distinct from :meth:`price`, which costs model-token usage."""
|
|
130
|
+
return self.retrieval.price(
|
|
131
|
+
channel=channel,
|
|
132
|
+
operation=operation,
|
|
133
|
+
units=units,
|
|
134
|
+
at=at,
|
|
135
|
+
region=region,
|
|
136
|
+
)
|
|
137
|
+
|
|
116
138
|
|
|
117
139
|
def _date(value: Any, *, field: str, model: str) -> datetime:
|
|
118
140
|
if value is None:
|
|
@@ -228,6 +250,79 @@ def parse_catalog(
|
|
|
228
250
|
return version, aliases, prices
|
|
229
251
|
|
|
230
252
|
|
|
253
|
+
def parse_retrieval(document: Any) -> list[RetrievalPrice]:
|
|
254
|
+
"""Parse the optional top-level ``retrieval`` list into effective-dated
|
|
255
|
+
per-1,000-unit prices, applying the same effective-date validation as the
|
|
256
|
+
token catalog (ISO dates, ``effective_to`` after ``effective_from``, no
|
|
257
|
+
overlapping windows per channel/operation/region). A per-unit rate must be
|
|
258
|
+
present and non-negative. An absent ``retrieval`` key yields no prices."""
|
|
259
|
+
if not isinstance(document, dict):
|
|
260
|
+
raise CatalogError("prices document must be a mapping")
|
|
261
|
+
entries = document.get("retrieval")
|
|
262
|
+
if entries is None:
|
|
263
|
+
return []
|
|
264
|
+
if not isinstance(entries, list):
|
|
265
|
+
raise CatalogError("prices document retrieval must be a list")
|
|
266
|
+
prices: list[RetrievalPrice] = []
|
|
267
|
+
seen_windows: list[tuple[str, str, str, datetime, datetime | None]] = []
|
|
268
|
+
for entry in entries:
|
|
269
|
+
channel = str(entry.get("channel") or "").strip().lower()
|
|
270
|
+
operation = str(entry.get("operation") or "").strip().lower()
|
|
271
|
+
region = str(entry.get("region") or "global")
|
|
272
|
+
unit = str(entry.get("unit") or "").strip()
|
|
273
|
+
if not channel or not operation:
|
|
274
|
+
raise CatalogError("retrieval entry needs channel/operation")
|
|
275
|
+
label = f"{channel}/{operation}"
|
|
276
|
+
if not unit:
|
|
277
|
+
raise CatalogError(f"{label}: retrieval entry needs a unit")
|
|
278
|
+
if not str(entry.get("source_url") or "").strip():
|
|
279
|
+
raise CatalogError(f"{label}: retrieval entry needs source_url")
|
|
280
|
+
per_1k_usd = _decimal(entry.get("per_1k_usd"))
|
|
281
|
+
if per_1k_usd is None or per_1k_usd < 0:
|
|
282
|
+
raise CatalogError(
|
|
283
|
+
f"{label}: retrieval entry needs a non-negative per_1k_usd"
|
|
284
|
+
)
|
|
285
|
+
effective_from = _date(
|
|
286
|
+
entry.get("effective_from"), field="effective_from", model=label
|
|
287
|
+
)
|
|
288
|
+
effective_to = (
|
|
289
|
+
_date(entry.get("effective_to"), field="effective_to", model=label)
|
|
290
|
+
if entry.get("effective_to") is not None
|
|
291
|
+
else None
|
|
292
|
+
)
|
|
293
|
+
if effective_to is not None and effective_to <= effective_from:
|
|
294
|
+
raise CatalogError(f"{label}: effective_to before effective_from")
|
|
295
|
+
for other in seen_windows:
|
|
296
|
+
other_channel, other_operation, other_region, other_from, other_to = other
|
|
297
|
+
if (other_channel, other_operation, other_region) != (
|
|
298
|
+
channel,
|
|
299
|
+
operation,
|
|
300
|
+
region,
|
|
301
|
+
):
|
|
302
|
+
continue
|
|
303
|
+
if (effective_to is None or other_from < effective_to) and (
|
|
304
|
+
other_to is None or effective_from < other_to
|
|
305
|
+
):
|
|
306
|
+
raise CatalogError(
|
|
307
|
+
f"{label}: overlapping {region} retrieval price windows"
|
|
308
|
+
)
|
|
309
|
+
seen_windows.append((channel, operation, region, effective_from, effective_to))
|
|
310
|
+
prices.append(
|
|
311
|
+
RetrievalPrice(
|
|
312
|
+
id=f"{channel}:{operation}:{region}:{effective_from.date()}",
|
|
313
|
+
channel=channel,
|
|
314
|
+
operation=operation,
|
|
315
|
+
region=region,
|
|
316
|
+
unit=unit,
|
|
317
|
+
per_1k_usd=per_1k_usd,
|
|
318
|
+
effective_from=effective_from,
|
|
319
|
+
effective_to=effective_to,
|
|
320
|
+
source_url=str(entry["source_url"]).strip(),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
return prices
|
|
324
|
+
|
|
325
|
+
|
|
231
326
|
def load_catalog(
|
|
232
327
|
path: str | Path | None = None, *, region: str = "global"
|
|
233
328
|
) -> LoadedCatalog:
|
|
@@ -244,4 +339,5 @@ def load_catalog(
|
|
|
244
339
|
document=document,
|
|
245
340
|
snapshot=CatalogSnapshot(aliases, prices, region=region),
|
|
246
341
|
canonical_ids=_canonical_index(document),
|
|
342
|
+
retrieval=RetrievalCatalog(parse_retrieval(document)),
|
|
247
343
|
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Effective-dated retrieval pricing: searches, tool calls, grounded queries.
|
|
2
|
+
|
|
3
|
+
Retrieval pricing sits alongside -- and deliberately apart from -- the
|
|
4
|
+
model-token catalog. A retrieval operation is billed per counted unit (a
|
|
5
|
+
completed web search, a tool call, an executed grounding query), never per
|
|
6
|
+
token, so it carries its own price shape and its own catalog object.
|
|
7
|
+
|
|
8
|
+
The contract mirrors the token path's explicit priced/partial/unpriced
|
|
9
|
+
semantics: a resolved operation with a valid unit count is ``priced`` (a $0
|
|
10
|
+
fee is a real priced zero), while a missing, non-integer, or negative unit
|
|
11
|
+
count and an unknown operation or channel come back ``unpriced`` with a reason
|
|
12
|
+
-- never silently priced at zero.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from .catalog import _COST_QUANTUM, _coerce_datetime
|
|
21
|
+
|
|
22
|
+
_THOUSAND = Decimal("1000")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class RetrievalPrice:
|
|
27
|
+
"""An effective-dated per-1,000-unit price for one channel/operation."""
|
|
28
|
+
|
|
29
|
+
id: str
|
|
30
|
+
channel: str
|
|
31
|
+
operation: str
|
|
32
|
+
region: str
|
|
33
|
+
unit: str
|
|
34
|
+
per_1k_usd: Decimal
|
|
35
|
+
effective_from: datetime
|
|
36
|
+
effective_to: datetime | None
|
|
37
|
+
source_url: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class RetrievalCostResult:
|
|
42
|
+
"""The outcome of pricing a retrieval operation.
|
|
43
|
+
|
|
44
|
+
``status`` reuses the token path's vocabulary (``priced``/``partial``/
|
|
45
|
+
``unpriced``); retrieval never produces ``partial`` because a unit count is
|
|
46
|
+
all-or-nothing -- a bad count leaves nothing knowable to price.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
price_id: str | None
|
|
50
|
+
cost_usd: Decimal | None
|
|
51
|
+
status: str
|
|
52
|
+
reasons: tuple[str, ...] = ()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _units(value: Any) -> tuple[int | None, str | None]:
|
|
56
|
+
"""Coerce a retrieval unit count under a strict-integer contract.
|
|
57
|
+
|
|
58
|
+
Only a real, non-negative ``int`` is a valid count. ``bool`` (``type`` is
|
|
59
|
+
``bool``, not ``int``), ``float``, strings, and ``Decimal`` are rejected as
|
|
60
|
+
``invalid_units`` rather than truncated or parsed; ``None`` is
|
|
61
|
+
``missing_units`` and a negative int is ``negative_units``. Each rejection
|
|
62
|
+
keeps the operation explicitly unpriced instead of turning it into zero.
|
|
63
|
+
"""
|
|
64
|
+
if value is None:
|
|
65
|
+
return None, "missing_units"
|
|
66
|
+
if type(value) is not int:
|
|
67
|
+
return None, "invalid_units"
|
|
68
|
+
if value < 0:
|
|
69
|
+
return None, "negative_units"
|
|
70
|
+
return value, None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RetrievalCatalog:
|
|
74
|
+
"""Resolve and cost retrieval operations by ``(channel, operation)``.
|
|
75
|
+
|
|
76
|
+
Channel selection is exact and region resolution falls back
|
|
77
|
+
``region -> "*" -> "global"``, mirroring the token snapshot: an operation a
|
|
78
|
+
channel does not carry stays unpriced rather than being repriced off another
|
|
79
|
+
channel or region.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, prices: list[RetrievalPrice]) -> None:
|
|
83
|
+
self._by_key: dict[tuple[str, str], list[RetrievalPrice]] = {}
|
|
84
|
+
self._channels: set[str] = set()
|
|
85
|
+
for price in prices:
|
|
86
|
+
self._channels.add(price.channel)
|
|
87
|
+
self._by_key.setdefault((price.channel, price.operation), []).append(price)
|
|
88
|
+
for candidates in self._by_key.values():
|
|
89
|
+
candidates.sort(key=lambda price: price.effective_from, reverse=True)
|
|
90
|
+
|
|
91
|
+
def _select(
|
|
92
|
+
self, channel: str, operation: str, region: str, at: datetime
|
|
93
|
+
) -> RetrievalPrice | None:
|
|
94
|
+
at = at if at.tzinfo else at.replace(tzinfo=timezone.utc)
|
|
95
|
+
candidates = self._by_key.get((channel, operation), [])
|
|
96
|
+
for candidate_region in dict.fromkeys((region, "*", "global")):
|
|
97
|
+
for price in candidates:
|
|
98
|
+
if price.region.lower() != candidate_region:
|
|
99
|
+
continue
|
|
100
|
+
if price.effective_from <= at and (
|
|
101
|
+
price.effective_to is None or at < price.effective_to
|
|
102
|
+
):
|
|
103
|
+
return price
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def price(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
channel: Any,
|
|
110
|
+
operation: Any,
|
|
111
|
+
units: Any,
|
|
112
|
+
at: Any,
|
|
113
|
+
region: Any = "global",
|
|
114
|
+
) -> RetrievalCostResult:
|
|
115
|
+
"""Price ``units`` of ``operation`` on ``channel`` at time ``at``.
|
|
116
|
+
|
|
117
|
+
Resolution happens before unit validation: an unknown channel or
|
|
118
|
+
operation is reported as such regardless of the unit count, because an
|
|
119
|
+
operation that cannot be identified cannot be priced at all.
|
|
120
|
+
"""
|
|
121
|
+
channel_key = str(channel or "").strip().lower()
|
|
122
|
+
operation_key = str(operation or "").strip().lower()
|
|
123
|
+
region_key = str(region or "global").strip().lower() or "global"
|
|
124
|
+
|
|
125
|
+
if channel_key not in self._channels:
|
|
126
|
+
return RetrievalCostResult(None, None, "unpriced", ("unknown_channel",))
|
|
127
|
+
if (channel_key, operation_key) not in self._by_key:
|
|
128
|
+
return RetrievalCostResult(None, None, "unpriced", ("unknown_operation",))
|
|
129
|
+
|
|
130
|
+
price = self._select(channel_key, operation_key, region_key, _coerce_datetime(at))
|
|
131
|
+
if price is None:
|
|
132
|
+
return RetrievalCostResult(None, None, "unpriced", ("no_effective_price",))
|
|
133
|
+
|
|
134
|
+
count, reason = _units(units)
|
|
135
|
+
if reason is not None:
|
|
136
|
+
return RetrievalCostResult(None, None, "unpriced", (reason,))
|
|
137
|
+
|
|
138
|
+
cost = (Decimal(count) * price.per_1k_usd / _THOUSAND).quantize(
|
|
139
|
+
_COST_QUANTUM, rounding=ROUND_HALF_UP
|
|
140
|
+
)
|
|
141
|
+
return RetrievalCostResult(price.id, cost, "priced", ())
|
|
@@ -5,6 +5,7 @@ src/metergraph_core/__init__.py
|
|
|
5
5
|
src/metergraph_core/billing.py
|
|
6
6
|
src/metergraph_core/catalog.py
|
|
7
7
|
src/metergraph_core/loader.py
|
|
8
|
+
src/metergraph_core/retrieval.py
|
|
8
9
|
src/metergraph_core.egg-info/PKG-INFO
|
|
9
10
|
src/metergraph_core.egg-info/SOURCES.txt
|
|
10
11
|
src/metergraph_core.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.1 → metergraph_core-0.2.2}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|