metergraph-core 0.2.1__tar.gz → 0.2.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Reusable MeterGraph catalog and deterministic billing engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "metergraph-core"
3
- version = "0.2.1"
3
+ version = "0.2.3"
4
4
  description = "Reusable MeterGraph catalog and deterministic billing engine"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.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-25"
9
+ version: "2026-08-27"
10
10
  currency: USD
11
- pricing_verified_at: "2026-08-25"
11
+ pricing_verified_at: "2026-08-27"
12
12
  models:
13
13
  - canonical_id: openai/gpt-5.6-sol
14
14
  publisher: openai
@@ -20,6 +20,7 @@ models:
20
20
  alias: openai/gpt-5.6-sol
21
21
  channel: vercel-ai-gateway
22
22
  rules: {input_includes_cache_read: true, input_includes_cache_write: true}
23
+ - {provider: bedrock, alias: us.openai.gpt-5.6-sol, channel: aws-bedrock}
23
24
  prices:
24
25
  - channel: openai-api
25
26
  region: global
@@ -44,6 +45,16 @@ models:
44
45
  rules:
45
46
  long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
46
47
  source_url: https://vercel.com/ai-gateway/models/gpt-5.6-sol/providers
48
+ - channel: aws-bedrock
49
+ region: us-west-2
50
+ effective_from: "2026-08-17"
51
+ input_per_mtok: 5.50
52
+ output_per_mtok: 33.00
53
+ cache_read_per_mtok: 0.55
54
+ cache_write_5m_per_mtok: 6.875
55
+ rules:
56
+ long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
57
+ source_url: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-sol.html
47
58
 
48
59
  - canonical_id: openai/gpt-5.6-terra
49
60
  publisher: openai
@@ -53,6 +64,7 @@ models:
53
64
  alias: openai/gpt-5.6-terra
54
65
  channel: vercel-ai-gateway
55
66
  rules: {input_includes_cache_read: true, input_includes_cache_write: true}
67
+ - {provider: bedrock, alias: us.openai.gpt-5.6-terra, channel: aws-bedrock}
56
68
  prices:
57
69
  - channel: openai-api
58
70
  region: global
@@ -77,6 +89,16 @@ models:
77
89
  rules:
78
90
  long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
79
91
  source_url: https://vercel.com/ai-gateway/models/gpt-5.6-terra/providers
92
+ - channel: aws-bedrock
93
+ region: us-west-2
94
+ effective_from: "2026-08-17"
95
+ input_per_mtok: 2.20
96
+ output_per_mtok: 13.20
97
+ cache_read_per_mtok: 0.22
98
+ cache_write_5m_per_mtok: 2.75
99
+ rules:
100
+ long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
101
+ source_url: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-terra.html
80
102
 
81
103
  - canonical_id: openai/gpt-5.6-luna
82
104
  publisher: openai
@@ -86,6 +108,7 @@ models:
86
108
  alias: openai/gpt-5.6-luna
87
109
  channel: vercel-ai-gateway
88
110
  rules: {input_includes_cache_read: true, input_includes_cache_write: true}
111
+ - {provider: bedrock, alias: us.openai.gpt-5.6-luna, channel: aws-bedrock}
89
112
  prices:
90
113
  - channel: openai-api
91
114
  region: global
@@ -125,6 +148,16 @@ models:
125
148
  rules:
126
149
  long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
127
150
  source_url: https://ai-gateway.vercel.sh/v1/models
151
+ - channel: aws-bedrock
152
+ region: us-west-2
153
+ effective_from: "2026-08-17"
154
+ input_per_mtok: 0.22
155
+ output_per_mtok: 1.32
156
+ cache_read_per_mtok: 0.022
157
+ cache_write_5m_per_mtok: 0.275
158
+ rules:
159
+ long_context: {threshold: 272000, input_multiplier: 2, output_multiplier: 1.5}
160
+ source_url: https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-56-luna.html
128
161
  - channel: vercel-ai-gateway
129
162
  region: global
130
163
  effective_from: "2026-07-30"
@@ -598,6 +631,18 @@ models:
598
631
  cache_read_per_mtok: 0.015
599
632
  source_url: https://ai-gateway.vercel.sh/v1/models
600
633
 
634
+ - canonical_id: google/gemma-3-27b-it
635
+ publisher: google
636
+ aliases:
637
+ - {provider: bedrock, alias: google.gemma-3-27b-it, channel: aws-bedrock}
638
+ prices:
639
+ - channel: aws-bedrock
640
+ region: us-west-2
641
+ effective_from: "2026-08-27"
642
+ input_per_mtok: 0.23
643
+ output_per_mtok: 0.38
644
+ source_url: https://aws.amazon.com/bedrock/pricing/
645
+
601
646
  - canonical_id: nvidia/nemotron-3-super-120b-a12b
602
647
  publisher: nvidia
603
648
  aliases:
@@ -653,6 +698,18 @@ models:
653
698
  output_per_mtok: 1.85
654
699
  source_url: https://aws.amazon.com/bedrock/pricing/
655
700
 
701
+ - canonical_id: deepseek/v3.1
702
+ publisher: deepseek
703
+ aliases:
704
+ - {provider: bedrock, alias: deepseek.v3-v1:0, channel: aws-bedrock}
705
+ prices:
706
+ - channel: aws-bedrock
707
+ region: us-west-2
708
+ effective_from: "2026-08-27"
709
+ input_per_mtok: 0.58
710
+ output_per_mtok: 1.68
711
+ source_url: https://aws.amazon.com/bedrock/pricing/
712
+
656
713
  - canonical_id: deepseek/v4-flash
657
714
  publisher: deepseek
658
715
  aliases:
@@ -904,6 +961,18 @@ models:
904
961
  cache_read_per_mtok: 0.29
905
962
  source_url: https://vercel.com/ai-gateway/models/kimi-k3/providers
906
963
 
964
+ - canonical_id: moonshotai/kimi-k2.5
965
+ publisher: moonshotai
966
+ aliases:
967
+ - {provider: bedrock, alias: moonshotai.kimi-k2.5, channel: aws-bedrock}
968
+ prices:
969
+ - channel: aws-bedrock
970
+ region: us-west-2
971
+ effective_from: "2026-08-27"
972
+ input_per_mtok: 0.60
973
+ output_per_mtok: 3.00
974
+ source_url: https://aws.amazon.com/bedrock/pricing/
975
+
907
976
  - canonical_id: minimax/minimax-m3
908
977
  publisher: minimax
909
978
  aliases:
@@ -975,3 +1044,39 @@ models:
975
1044
  output_per_mtok: 0.50
976
1045
  cache_read_per_mtok: 0.05
977
1046
  source_url: https://vercel.com/ai-gateway/models/grok-4.1-fast-reasoning/providers
1047
+
1048
+ # Retrieval prices: USD per 1,000 counted operations (completed searches, tool
1049
+ # calls, executed grounding queries), billed on the provider's direct channel.
1050
+ # These sit alongside the model-token catalog above but are priced per unit, not
1051
+ # per token. Effective-dated like model prices: keep superseded entries and close
1052
+ # them with `effective_to` rather than editing in place. A $0 fee is an explicit
1053
+ # priced zero, not the absence of a price.
1054
+ retrieval:
1055
+ - channel: anthropic-api
1056
+ operation: web_search
1057
+ region: global
1058
+ unit: completed_search
1059
+ per_1k_usd: 10.00
1060
+ effective_from: "2026-08-26"
1061
+ source_url: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-search-tool
1062
+ - channel: anthropic-api
1063
+ operation: web_fetch
1064
+ region: global
1065
+ unit: tool_call
1066
+ per_1k_usd: 0.00
1067
+ effective_from: "2026-08-26"
1068
+ source_url: https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/web-fetch-tool
1069
+ - channel: openai-api
1070
+ operation: web_search
1071
+ region: global
1072
+ unit: tool_call
1073
+ per_1k_usd: 10.00
1074
+ effective_from: "2026-08-26"
1075
+ source_url: https://platform.openai.com/docs/pricing
1076
+ - channel: google-api
1077
+ operation: google_search_grounding
1078
+ region: global
1079
+ unit: executed_query
1080
+ per_1k_usd: 14.00
1081
+ effective_from: "2026-08-26"
1082
+ 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", ())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Reusable MeterGraph catalog and deterministic billing engine
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -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