metergraph-core 0.8.4__tar.gz → 0.8.6__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.
Files changed (25) hide show
  1. {metergraph_core-0.8.4/src/metergraph_core.egg-info → metergraph_core-0.8.6}/PKG-INFO +31 -1
  2. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/README.md +30 -0
  3. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/pyproject.toml +1 -1
  4. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/__init__.py +6 -0
  5. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/billing.py +5 -1
  6. metergraph_core-0.8.6/src/metergraph_core/calls.py +188 -0
  7. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/data/models.yaml +32 -1
  8. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/data/prices.yaml +163 -2
  9. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/extractors.py +7 -2
  10. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/loader.py +17 -8
  11. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/models.py +6 -2
  12. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/retrieval.py +2 -1
  13. {metergraph_core-0.8.4 → metergraph_core-0.8.6/src/metergraph_core.egg-info}/PKG-INFO +31 -1
  14. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core.egg-info/SOURCES.txt +1 -0
  15. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/MANIFEST.in +0 -0
  16. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/setup.cfg +0 -0
  17. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/catalog.py +0 -0
  18. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/catalog_delta.py +0 -0
  19. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/data/sources.yaml +0 -0
  20. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/refresh.py +0 -0
  21. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/responses.py +0 -0
  22. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core/sources.py +0 -0
  23. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
  24. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/src/metergraph_core.egg-info/requires.txt +0 -0
  25. {metergraph_core-0.8.4 → metergraph_core-0.8.6}/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.8.4
3
+ Version: 0.8.6
4
4
  Summary: Open, effective-dated LLM pricing catalog and deterministic cost calculator for popular models across major providers and gateways, including OpenAI, Anthropic, Google Gemini, AWS Bedrock, OpenRouter, Vercel AI Gateway, and more.
5
5
  Author: Metergraph
6
6
  License-Expression: Apache-2.0
@@ -603,3 +603,33 @@ lifecycles.
603
603
  The model registry version is an ISO date. When more than one registry ships on
604
604
  the same date, append a positive revision such as `2026-09-25.2` so consumers
605
605
  can distinguish the policies while pricing validation still uses that date.
606
+
607
+
608
+ ## Complete call pricing
609
+
610
+ `price_call_from_response` combines token costs with supported retrieval fees.
611
+ `cost_from_response` remains token-only. Retrieval pricing supports Anthropic
612
+ `web_search` and `web_fetch` on `anthropic-api`; other observed scopes are unresolved.
613
+
614
+ ```python
615
+ from metergraph_core import price_call_from_response
616
+
617
+ result = price_call_from_response(
618
+ "anthropic",
619
+ {"type": "message", "model": "claude-sonnet-5-5", "usage": {
620
+ "input_tokens": 1000, "output_tokens": 100,
621
+ "server_tool_use": {"web_search_requests": 2},
622
+ }},
623
+ at="2026-10-20T00:00:00Z",
624
+ )
625
+ assert str(result.cost_usd) == "0.02300000"
626
+ ```
627
+
628
+ `cost_usd` is set only when the call is fully priced. `known_subtotal_usd`
629
+ contains exact components; `estimated_total_usd` separates token fallback
630
+ estimates when all retrieval fees are known. Missing charges are reported in
631
+ `missing_dimensions`. Counts must be nonnegative integers.
632
+
633
+ For normalized usage, use `price_call` with `ModelUsage` and `RetrievalUsage`.
634
+ To compose an existing token result from the same catalog, use `compose_call_cost`.
635
+ `resolve_billing` accepts complete-call results for gateway reconciliation.
@@ -573,3 +573,33 @@ lifecycles.
573
573
  The model registry version is an ISO date. When more than one registry ships on
574
574
  the same date, append a positive revision such as `2026-09-25.2` so consumers
575
575
  can distinguish the policies while pricing validation still uses that date.
576
+
577
+
578
+ ## Complete call pricing
579
+
580
+ `price_call_from_response` combines token costs with supported retrieval fees.
581
+ `cost_from_response` remains token-only. Retrieval pricing supports Anthropic
582
+ `web_search` and `web_fetch` on `anthropic-api`; other observed scopes are unresolved.
583
+
584
+ ```python
585
+ from metergraph_core import price_call_from_response
586
+
587
+ result = price_call_from_response(
588
+ "anthropic",
589
+ {"type": "message", "model": "claude-sonnet-5-5", "usage": {
590
+ "input_tokens": 1000, "output_tokens": 100,
591
+ "server_tool_use": {"web_search_requests": 2},
592
+ }},
593
+ at="2026-10-20T00:00:00Z",
594
+ )
595
+ assert str(result.cost_usd) == "0.02300000"
596
+ ```
597
+
598
+ `cost_usd` is set only when the call is fully priced. `known_subtotal_usd`
599
+ contains exact components; `estimated_total_usd` separates token fallback
600
+ estimates when all retrieval fees are known. Missing charges are reported in
601
+ `missing_dimensions`. Counts must be nonnegative integers.
602
+
603
+ For normalized usage, use `price_call` with `ModelUsage` and `RetrievalUsage`.
604
+ To compose an existing token result from the same catalog, use `compose_call_cost`.
605
+ `resolve_billing` accepts complete-call results for gateway reconciliation.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "metergraph-core"
3
- version = "0.8.4"
3
+ version = "0.8.6"
4
4
  description = "Open, effective-dated LLM pricing catalog and deterministic cost calculator for popular models across major providers and gateways, including OpenAI, Anthropic, Google Gemini, AWS Bedrock, OpenRouter, Vercel AI Gateway, and more."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -155,3 +155,9 @@ __all__ = [
155
155
  "search_operation_for_channel",
156
156
  "validate_model_registry",
157
157
  ]
158
+
159
+ from .calls import (CallCostResult, RetrievalComponent, RetrievalUsage, compose_call_cost,
160
+ price_call, price_call_from_response, response_retrieval_usage)
161
+
162
+ __all__ += ["CallCostResult", "RetrievalComponent", "RetrievalUsage", "compose_call_cost",
163
+ "price_call", "price_call_from_response", "response_retrieval_usage"]
@@ -168,7 +168,7 @@ def _disputed(
168
168
 
169
169
 
170
170
  def resolve_billing(
171
- catalog_result: CostResult,
171
+ catalog_result: "CostResult | CallCostResult",
172
172
  evidence: GatewayBillingEvidence,
173
173
  *,
174
174
  disputes: Iterable[Dispute] = (),
@@ -231,3 +231,7 @@ def resolve_billing(
231
231
  catalog_price_id=catalog_result.price_id,
232
232
  catalog_reasons=reasons,
233
233
  )
234
+
235
+ from typing import TYPE_CHECKING
236
+ if TYPE_CHECKING:
237
+ from .calls import CallCostResult
@@ -0,0 +1,188 @@
1
+ """Compose token and counted retrieval costs from one catalog snapshot."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from decimal import Decimal
6
+ from typing import Any, Mapping, Sequence
7
+
8
+ from .catalog import CostResult, ModelUsage, direct_channel_for_provider, normalize_provider
9
+ from .loader import LoadedCatalog
10
+ from .responses import _as_mapping, _bundled_catalog, _first, cost_from_response, response_format
11
+
12
+ _ZERO = Decimal('0')
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class RetrievalUsage:
17
+ operation: str
18
+ units: Any
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class RetrievalComponent:
23
+ operation: str
24
+ units: Any
25
+ cost_usd: Decimal | None
26
+ price_id: str | None
27
+ basis: str
28
+ reasons: tuple[str, ...] = ()
29
+ source_url: str | None = None
30
+
31
+
32
+ @dataclass(frozen=True, slots=True)
33
+ class CallCostResult:
34
+ token_cost: CostResult
35
+ retrieval: tuple[RetrievalComponent, ...]
36
+ cost_usd: Decimal | None
37
+ known_subtotal_usd: Decimal
38
+ estimated_total_usd: Decimal | None
39
+ status: str
40
+ token_basis: str
41
+ missing_dimensions: tuple[str, ...]
42
+ catalog_hash: str
43
+
44
+
45
+ @property
46
+ def publisher(self) -> str | None:
47
+ return self.token_cost.publisher
48
+
49
+ @property
50
+ def price_id(self) -> str | None:
51
+ return self.token_cost.price_id
52
+
53
+ @property
54
+ def reasons(self) -> tuple[str, ...]:
55
+ return tuple(dict.fromkeys((*self.token_cost.reasons,
56
+ *(f"missing_{d}" for d in self.missing_dimensions))))
57
+
58
+
59
+ def compose_call_cost(
60
+ token_cost: CostResult, *, catalog: LoadedCatalog,
61
+ retrieval: Sequence[RetrievalUsage] = (), provider: str | None = None,
62
+ channel: str | None = None, region: str = 'global',
63
+ ) -> CallCostResult:
64
+ """Combine retrieval with a token result from the same catalog and time.
65
+
66
+ Partial token results remain estimates, excluded from the known subtotal.
67
+ """
68
+ if token_cost.effective_at is None:
69
+ raise ValueError('token_cost must carry its effective_at')
70
+ if token_cost.price is not None:
71
+ selected = catalog.lookup_price(model=token_cost.canonical_model,
72
+ channel=token_cost.price.pricing_channel, region=token_cost.price.region,
73
+ at=token_cost.effective_at)
74
+ if selected.price != token_cost.price:
75
+ raise ValueError('token price does not match the catalog snapshot')
76
+ if len({item.operation for item in retrieval}) != len(retrieval):
77
+ raise ValueError('retrieval operations must be aggregated once per call')
78
+ resolved_channel = token_cost.price.pricing_channel if token_cost.price else (channel or direct_channel_for_provider(provider))
79
+ publisher = token_cost.publisher or normalize_provider(provider or '')
80
+ missing = list(token_cost.missing_dimensions)
81
+ token_basis = ('exact' if token_cost.status == 'priced' else
82
+ 'fallback_estimate' if token_cost.cost_usd is not None else 'unknown')
83
+ # Retrieval rates are USD. Do not add them to a different currency.
84
+ currency_ok = token_cost.currency in (None, 'USD')
85
+ known = token_cost.cost_usd if token_basis == 'exact' and currency_ok else _ZERO
86
+ if not currency_ok:
87
+ missing.append('currency')
88
+ token_basis = 'unknown'
89
+ parts = []
90
+ for usage in retrieval:
91
+ if resolved_channel == 'anthropic-api' and publisher == 'anthropic' and usage.operation in {'web_search', 'web_fetch'}:
92
+ fee = catalog.price_retrieval(channel=resolved_channel, operation=usage.operation,
93
+ units=usage.units, at=token_cost.effective_at, region=region)
94
+ part = RetrievalComponent(usage.operation, usage.units, fee.cost_usd,
95
+ fee.price_id, 'exact' if fee.status == 'priced' else 'unknown', fee.reasons, fee.source_url)
96
+ else:
97
+ part = RetrievalComponent(usage.operation, usage.units, None, None,
98
+ 'unknown', ('unsupported_retrieval_scope',))
99
+ part = RetrievalComponent(part.operation,
100
+ part.units if type(part.units) is int and part.units >= 0 else None,
101
+ part.cost_usd, part.price_id, part.basis, part.reasons, part.source_url)
102
+ parts.append(part)
103
+ if part.cost_usd is None:
104
+ missing.append(f'retrieval.{usage.operation}')
105
+ else:
106
+ known += part.cost_usd
107
+ unresolved = any(p.basis == 'unknown' for p in parts)
108
+ status = ('unpriced' if token_cost.price is None or token_cost.status == 'unpriced' else
109
+ 'partial' if token_basis != 'exact' or unresolved or missing else 'priced')
110
+ estimated = None
111
+ if token_basis == 'fallback_estimate' and not unresolved:
112
+ estimated = token_cost.cost_usd + sum((p.cost_usd or _ZERO for p in parts), _ZERO)
113
+ return CallCostResult(token_cost, tuple(parts), known if status == 'priced' else None,
114
+ known, estimated, status, token_basis, tuple(dict.fromkeys(missing)), catalog.content_hash)
115
+
116
+
117
+ def price_call(
118
+ *, model: Any, usage: ModelUsage, at: Any, catalog: LoadedCatalog | None = None,
119
+ provider: str | None = None, channel: str | None = None, region: str | None = None,
120
+ retrieval: Sequence[RetrievalUsage] = (),
121
+ ) -> CallCostResult:
122
+ """Price normalized token and retrieval usage against one snapshot."""
123
+ loaded = catalog if catalog is not None else _bundled_catalog()
124
+ tokens = loaded.calculate_cost(model=model, usage=usage, at=at, provider=provider,
125
+ channel=channel, region=region)
126
+ return compose_call_cost(tokens, catalog=loaded, retrieval=retrieval, provider=provider,
127
+ channel=channel or direct_channel_for_provider(provider), region=region or 'global')
128
+
129
+
130
+ def response_retrieval_usage(response: Any, *, format: str | None = None) -> tuple[RetrievalUsage, ...]:
131
+ """Read operation metadata without reading prompt or response text.
132
+
133
+ Tool evidence without a valid usage counter remains unresolved.
134
+ """
135
+ data = _as_mapping(response)
136
+ detected = format or response_format(data)
137
+ if detected == 'anthropic.messages':
138
+ usage = data.get('usage')
139
+ usage = usage if isinstance(usage, Mapping) else {}
140
+ server = usage.get('server_tool_use')
141
+ if server is not None and not isinstance(server, Mapping):
142
+ return (RetrievalUsage('web_search', None),)
143
+ server = server or {}
144
+ found = [RetrievalUsage(op, server[key]) for key, op in (
145
+ ('web_search_requests', 'web_search'), ('web_fetch_requests', 'web_fetch')) if key in server]
146
+ observed = set()
147
+ content = data.get('content')
148
+ if isinstance(content, list):
149
+ for block in content:
150
+ if isinstance(block, Mapping) and block.get('type') == 'server_tool_use':
151
+ name = block.get('name')
152
+ if name in ('web_search', 'web_fetch'):
153
+ observed.add(name)
154
+ found.extend(RetrievalUsage(op, None) for op in sorted(observed) if op not in {f.operation for f in found})
155
+ return tuple(found)
156
+ if detected == 'gemini.generate_content':
157
+ candidates = data.get('candidates')
158
+ if isinstance(candidates, list):
159
+ for item in candidates:
160
+ metadata = _first(item, 'groundingMetadata', 'grounding_metadata') if isinstance(item, Mapping) else None
161
+ if isinstance(metadata, Mapping) and any(
162
+ _first(metadata, camel, snake) is not None
163
+ for camel, snake in (("webSearchQueries", "web_search_queries"),
164
+ ("groundingChunks", "grounding_chunks"))
165
+ ):
166
+ # Grounding evidence does not establish billable query count.
167
+ return (RetrievalUsage('google_search_grounding', None),)
168
+ if detected == 'openai.responses':
169
+ output = data.get('output')
170
+ if isinstance(output, list):
171
+ count = sum(1 for item in output if isinstance(item, Mapping) and item.get('type') == 'web_search_call')
172
+ if count:
173
+ return (RetrievalUsage('web_search', count),)
174
+ return ()
175
+
176
+
177
+ def price_call_from_response(
178
+ provider: str, response: Any, *, catalog: LoadedCatalog | None = None,
179
+ model: str | None = None, at: Any = None, channel: str | None = None,
180
+ region: str | None = None, service_tier: str | None = None, format: str | None = None,
181
+ ) -> CallCostResult:
182
+ """Price a provider response, including supported counted retrieval fees."""
183
+ loaded = catalog if catalog is not None else _bundled_catalog()
184
+ tokens = cost_from_response(provider, response, catalog=loaded, model=model, at=at,
185
+ channel=channel, region=region, service_tier=service_tier, format=format)
186
+ return compose_call_cost(tokens, catalog=loaded,
187
+ retrieval=response_retrieval_usage(response, format=format), provider=provider,
188
+ channel=channel or direct_channel_for_provider(provider), region=region or 'global')
@@ -1,4 +1,4 @@
1
- version: '2026-10-27'
1
+ version: '2026-10-29'
2
2
  models:
3
3
  - canonical_id: openai/gpt-5.6-sol
4
4
  display_name: GPT-5.6 Sol
@@ -888,6 +888,30 @@ models:
888
888
  execution_profiles:
889
889
  - default
890
890
  display_name: Kimi K3.1 Pro Highspeed (direct)
891
+ - canonical_id: minimax/minimax-m3.1-flash
892
+ display_name: MiniMax M3.1 Flash
893
+ publisher: minimax
894
+ released: '2026-10-19' # https://www.minimax.io/blog/minimax-m31f
895
+ routes:
896
+ - key: default:minimax/minimax-m3.1-flash
897
+ id: minimax/minimax-m3.1-flash
898
+ provider: vercel
899
+ model_id: minimax/minimax-m3.1-flash
900
+ pricing_channel: vercel-ai-gateway
901
+ execution_profiles:
902
+ - default
903
+ - canonical_id: mistral/mistral-small-4
904
+ display_name: Mistral Small 4
905
+ publisher: mistral
906
+ released: '2026-03-16' # https://docs.mistral.ai/models/mistral-small-4-0-26-03
907
+ routes:
908
+ - key: default:mistral:mistral-small-2603
909
+ id: mistral:mistral-small-2603
910
+ provider: mistral
911
+ model_id: mistral-small-2603
912
+ pricing_channel: mistral-api
913
+ execution_profiles:
914
+ - default
891
915
  execution_profiles:
892
916
  - id: default
893
917
  routes:
@@ -963,6 +987,8 @@ execution_profiles:
963
987
  - default:openai:gpt-6.1-terra
964
988
  - default:moonshotai:kimi-k3.1-pro
965
989
  - default:moonshotai:kimi-k3.1-pro-highspeed
990
+ - default:minimax/minimax-m3.1-flash
991
+ - default:mistral:mistral-small-2603
966
992
  - id: bedrock
967
993
  routes:
968
994
  - bedrock:anthropic/claude-sonnet-5
@@ -1029,6 +1055,7 @@ offer_groups:
1029
1055
  - default:deepseek/deepseek-v4.1-flash-1017
1030
1056
  - default:deepseek/deepseek-v4.1-flash-1017-fast
1031
1057
  - default:inception/mercury-3
1058
+ - default:minimax/minimax-m3.1-flash
1032
1059
  - id: fireworks
1033
1060
  credential: FIREWORKS_API_KEY
1034
1061
  routes:
@@ -1059,6 +1086,10 @@ offer_groups:
1059
1086
  routes:
1060
1087
  - default:moonshotai:kimi-k3.1-pro
1061
1088
  - default:moonshotai:kimi-k3.1-pro-highspeed
1089
+ - id: mistral-direct
1090
+ credential: MISTRAL_API_KEY
1091
+ routes:
1092
+ - default:mistral:mistral-small-2603
1062
1093
  - id: bedrock
1063
1094
  credential: null
1064
1095
  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-29"
9
+ version: "2026-10-29.3"
10
10
  currency: USD
11
- pricing_verified_at: "2026-10-27"
11
+ pricing_verified_at: "2026-10-29"
12
12
  models:
13
13
  - canonical_id: openai/gpt-6-sol
14
14
  publisher: openai
@@ -1579,6 +1579,7 @@ models:
1579
1579
  - channel: anthropic-api
1580
1580
  region: global
1581
1581
  effective_from: "2026-10-07"
1582
+ effective_to: "2026-10-29"
1582
1583
  input_per_mtok: 2.00
1583
1584
  output_per_mtok: 10.00
1584
1585
  cache_read_per_mtok: 0.10
@@ -1587,6 +1588,17 @@ models:
1587
1588
  batch_input_per_mtok: 1.00
1588
1589
  batch_output_per_mtok: 5.00
1589
1590
  source_url: https://www.anthropic.com/claude-haiku-5-5
1591
+ - channel: anthropic-api
1592
+ region: global
1593
+ effective_from: "2026-10-29"
1594
+ input_per_mtok: 2.00
1595
+ output_per_mtok: 10.00
1596
+ cache_read_per_mtok: 0.10
1597
+ cache_write_5m_per_mtok: 2.50
1598
+ cache_write_1h_per_mtok: 4.00
1599
+ batch_input_per_mtok: 1.00
1600
+ batch_output_per_mtok: 5.00
1601
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
1590
1602
  - channel: vercel-ai-gateway
1591
1603
  region: global
1592
1604
  effective_from: "2026-10-07"
@@ -2436,12 +2448,24 @@ models:
2436
2448
  - channel: mistral-api
2437
2449
  region: global
2438
2450
  effective_from: "2026-10-20"
2451
+ effective_to: "2026-10-29"
2439
2452
  input_per_mtok: 1.36
2440
2453
  output_per_mtok: 4.18
2441
2454
  cache_read_per_mtok: 0.14
2442
2455
  batch_input_per_mtok: 0.68
2443
2456
  batch_output_per_mtok: 2.09
2444
2457
  source_url: https://docs.mistral.ai/models/mistral-large
2458
+ # Batch input/output use the documented 50% discount on all models:
2459
+ # https://docs.mistral.ai/studio/batch-processing
2460
+ - channel: mistral-api
2461
+ region: global
2462
+ effective_from: "2026-10-29"
2463
+ input_per_mtok: 0.70
2464
+ output_per_mtok: 2.10
2465
+ cache_read_per_mtok: 0.07
2466
+ batch_input_per_mtok: 0.35
2467
+ batch_output_per_mtok: 1.05
2468
+ source_url: https://docs.mistral.ai/models/mistral-large
2445
2469
  - channel: vercel-ai-gateway
2446
2470
  region: global
2447
2471
  effective_from: "2026-10-06"
@@ -2915,6 +2939,9 @@ models:
2915
2939
  rules:
2916
2940
  uncaptured_fees: true
2917
2941
  source_url: https://docs.perplexity.ai/docs/getting-started/pricing
2942
+ # The Agent API's model list is a separate product: its Sonar rates,
2943
+ # including its cache read rate, do not price this channel, so the
2944
+ # direct token rates above stay the last known ones.
2918
2945
  - channel: vercel-ai-gateway
2919
2946
  region: global
2920
2947
  effective_from: "2026-10-11"
@@ -3661,10 +3688,22 @@ models:
3661
3688
  - channel: moonshot-api
3662
3689
  region: global
3663
3690
  effective_from: "2026-07-16"
3691
+ effective_to: "2026-10-29"
3664
3692
  input_per_mtok: 3.0
3665
3693
  output_per_mtok: 15.0
3666
3694
  cache_read_per_mtok: 0.3
3667
3695
  source_url: https://platform.kimi.ai/docs/pricing/chat
3696
+ # The platform's cache-write figure is stored for the default 5m TTL;
3697
+ # the separately billed 1h tier remains unknown.
3698
+ - channel: moonshot-api
3699
+ region: global
3700
+ effective_from: "2026-10-29"
3701
+ input_per_mtok: 3.0
3702
+ output_per_mtok: 15.0
3703
+ cache_read_per_mtok: 0.3
3704
+ cache_write_5m_per_mtok: 3.0
3705
+ rules: {input_includes_cache_read: true, input_includes_cache_write: true}
3706
+ source_url: https://platform.kimi.ai/
3668
3707
  - channel: fireworks-api
3669
3708
  region: global
3670
3709
  effective_from: "2026-07-19"
@@ -4574,6 +4613,22 @@ models:
4574
4613
  - channel: google-api
4575
4614
  region: global
4576
4615
  effective_from: "2026-08-27"
4616
+ effective_to: "2026-10-28"
4617
+ input_per_mtok: 1.50
4618
+ output_per_mtok: 17.50
4619
+ source_url: https://ai.google.dev/gemini-api/docs/pricing
4620
+ - channel: google-api
4621
+ region: global
4622
+ effective_from: "2026-10-28"
4623
+ effective_to: "2026-10-29"
4624
+ # Reconcile the already-published live interval; this was an automated
4625
+ # text-rate misclassification, corrected by the successor below.
4626
+ input_per_mtok: 1.5
4627
+ output_per_mtok: 9
4628
+ source_url: https://ai.google.dev/gemini-api/docs/pricing
4629
+ - channel: google-api
4630
+ region: global
4631
+ effective_from: "2026-10-29"
4577
4632
  input_per_mtok: 1.50
4578
4633
  output_per_mtok: 17.50
4579
4634
  source_url: https://ai.google.dev/gemini-api/docs/pricing
@@ -5085,6 +5140,7 @@ models:
5085
5140
  - channel: anthropic-api
5086
5141
  region: global
5087
5142
  effective_from: "2026-06-09"
5143
+ effective_to: "2026-10-29"
5088
5144
  input_per_mtok: 10
5089
5145
  output_per_mtok: 50
5090
5146
  cache_read_per_mtok: 1
@@ -5093,6 +5149,17 @@ models:
5093
5149
  batch_input_per_mtok: 5
5094
5150
  batch_output_per_mtok: 25
5095
5151
  source_url: https://ai-gateway.vercel.sh/v1/models
5152
+ - channel: anthropic-api
5153
+ region: global
5154
+ effective_from: "2026-10-29"
5155
+ input_per_mtok: 10
5156
+ output_per_mtok: 50
5157
+ cache_read_per_mtok: 1
5158
+ cache_write_5m_per_mtok: 12.5
5159
+ cache_write_1h_per_mtok: 20
5160
+ batch_input_per_mtok: 5
5161
+ batch_output_per_mtok: 25
5162
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
5096
5163
  - canonical_id: anthropic/claude-opus-4
5097
5164
  publisher: anthropic
5098
5165
  aliases:
@@ -5113,6 +5180,7 @@ models:
5113
5180
  - channel: anthropic-api
5114
5181
  region: global
5115
5182
  effective_from: "2025-05-14"
5183
+ effective_to: "2026-10-29"
5116
5184
  input_per_mtok: 15
5117
5185
  output_per_mtok: 75
5118
5186
  cache_read_per_mtok: 1.5
@@ -5121,6 +5189,17 @@ models:
5121
5189
  batch_input_per_mtok: 7.5
5122
5190
  batch_output_per_mtok: 37.5
5123
5191
  source_url: https://ai-gateway.vercel.sh/v1/models
5192
+ - channel: anthropic-api
5193
+ region: global
5194
+ effective_from: "2026-10-29"
5195
+ input_per_mtok: 15
5196
+ output_per_mtok: 75
5197
+ cache_read_per_mtok: 1.5
5198
+ cache_write_5m_per_mtok: 18.75
5199
+ cache_write_1h_per_mtok: 30
5200
+ batch_input_per_mtok: 7.5
5201
+ batch_output_per_mtok: 37.5
5202
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
5124
5203
  - channel: vercel-ai-gateway
5125
5204
  region: global
5126
5205
  effective_from: "2026-10-02"
@@ -5148,6 +5227,7 @@ models:
5148
5227
  - channel: anthropic-api
5149
5228
  region: global
5150
5229
  effective_from: "2025-08-05"
5230
+ effective_to: "2026-10-29"
5151
5231
  input_per_mtok: 15
5152
5232
  output_per_mtok: 75
5153
5233
  cache_read_per_mtok: 1.5
@@ -5156,6 +5236,17 @@ models:
5156
5236
  batch_input_per_mtok: 7.5
5157
5237
  batch_output_per_mtok: 37.5
5158
5238
  source_url: https://ai-gateway.vercel.sh/v1/models
5239
+ - channel: anthropic-api
5240
+ region: global
5241
+ effective_from: "2026-10-29"
5242
+ input_per_mtok: 15
5243
+ output_per_mtok: 75
5244
+ cache_read_per_mtok: 1.5
5245
+ cache_write_5m_per_mtok: 18.75
5246
+ cache_write_1h_per_mtok: 30
5247
+ batch_input_per_mtok: 7.5
5248
+ batch_output_per_mtok: 37.5
5249
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
5159
5250
  - canonical_id: anthropic/claude-opus-4.5
5160
5251
  publisher: anthropic
5161
5252
  aliases:
@@ -5166,6 +5257,7 @@ models:
5166
5257
  - channel: anthropic-api
5167
5258
  region: global
5168
5259
  effective_from: "2025-11-01"
5260
+ effective_to: "2026-10-29"
5169
5261
  input_per_mtok: 5
5170
5262
  output_per_mtok: 25
5171
5263
  cache_read_per_mtok: 0.5
@@ -5174,6 +5266,17 @@ models:
5174
5266
  batch_input_per_mtok: 2.5
5175
5267
  batch_output_per_mtok: 12.5
5176
5268
  source_url: https://ai-gateway.vercel.sh/v1/models
5269
+ - channel: anthropic-api
5270
+ region: global
5271
+ effective_from: "2026-10-29"
5272
+ input_per_mtok: 5
5273
+ output_per_mtok: 25
5274
+ cache_read_per_mtok: 0.5
5275
+ cache_write_5m_per_mtok: 6.25
5276
+ cache_write_1h_per_mtok: 10
5277
+ batch_input_per_mtok: 2.5
5278
+ batch_output_per_mtok: 12.5
5279
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
5177
5280
  - channel: vercel-ai-gateway
5178
5281
  region: global
5179
5282
  effective_from: "2025-11-01"
@@ -5236,6 +5339,7 @@ models:
5236
5339
  - channel: anthropic-api
5237
5340
  region: global
5238
5341
  effective_from: "2025-05-14"
5342
+ effective_to: "2026-10-29"
5239
5343
  input_per_mtok: 3
5240
5344
  output_per_mtok: 15
5241
5345
  cache_read_per_mtok: 0.3
@@ -5244,6 +5348,17 @@ models:
5244
5348
  batch_input_per_mtok: 1.5
5245
5349
  batch_output_per_mtok: 7.5
5246
5350
  source_url: https://ai-gateway.vercel.sh/v1/models
5351
+ - channel: anthropic-api
5352
+ region: global
5353
+ effective_from: "2026-10-29"
5354
+ input_per_mtok: 3
5355
+ output_per_mtok: 15
5356
+ cache_read_per_mtok: 0.3
5357
+ cache_write_5m_per_mtok: 3.75
5358
+ cache_write_1h_per_mtok: 6
5359
+ batch_input_per_mtok: 1.5
5360
+ batch_output_per_mtok: 7.5
5361
+ source_url: https://platform.claude.com/docs/en/about-claude/pricing
5247
5362
  - channel: vercel-ai-gateway
5248
5363
  region: global
5249
5364
  effective_from: "2025-05-14"
@@ -5416,6 +5531,9 @@ models:
5416
5531
  input_per_mtok: 0.15
5417
5532
  output_per_mtok: 0.6
5418
5533
  cache_read_per_mtok: 0.015
5534
+ # prompt_tokens includes cached_tokens: https://docs.mistral.ai/studio/conversations/advanced/prompt-caching
5535
+ rules:
5536
+ input_includes_cache_read: true
5419
5537
  source_url: https://docs.mistral.ai/inference/pricing
5420
5538
  - channel: openrouter-api
5421
5539
  region: global
@@ -5743,12 +5861,22 @@ models:
5743
5861
  - channel: xai-api
5744
5862
  region: global
5745
5863
  effective_from: "2026-03-10"
5864
+ effective_to: "2026-10-29"
5746
5865
  input_per_mtok: 1.25
5747
5866
  output_per_mtok: 2.5
5748
5867
  cache_read_per_mtok: 0.2
5749
5868
  rules:
5750
5869
  long_context: {threshold: 200000, input_multiplier: 2.0, output_multiplier: 2.0}
5751
5870
  source_url: https://ai-gateway.vercel.sh/v1/models
5871
+ - channel: xai-api
5872
+ region: global
5873
+ effective_from: "2026-10-29"
5874
+ input_per_mtok: 1.25
5875
+ output_per_mtok: 2.5
5876
+ cache_read_per_mtok: 0.2
5877
+ rules:
5878
+ long_context: {threshold: 200000, input_multiplier: 2.0, output_multiplier: 2.0}
5879
+ source_url: https://docs.x.ai/developers/models
5752
5880
  - canonical_id: xai/grok-4.20-reasoning
5753
5881
  publisher: xai
5754
5882
  aliases:
@@ -5781,12 +5909,22 @@ models:
5781
5909
  - channel: xai-api
5782
5910
  region: global
5783
5911
  effective_from: "2026-03-10"
5912
+ effective_to: "2026-10-29"
5784
5913
  input_per_mtok: 1.25
5785
5914
  output_per_mtok: 2.5
5786
5915
  cache_read_per_mtok: 0.2
5787
5916
  rules:
5788
5917
  long_context: {threshold: 200000, input_multiplier: 2.0, output_multiplier: 2.0}
5789
5918
  source_url: https://ai-gateway.vercel.sh/v1/models
5919
+ - channel: xai-api
5920
+ region: global
5921
+ effective_from: "2026-10-29"
5922
+ input_per_mtok: 1.25
5923
+ output_per_mtok: 2.5
5924
+ cache_read_per_mtok: 0.2
5925
+ rules:
5926
+ long_context: {threshold: 200000, input_multiplier: 2.0, output_multiplier: 2.0}
5927
+ source_url: https://docs.x.ai/developers/models
5790
5928
  - channel: openrouter-api
5791
5929
  region: global
5792
5930
  effective_from: "2026-10-29"
@@ -6147,6 +6285,10 @@ models:
6147
6285
  alias: minimax/minimax-m3.1-flash
6148
6286
  channel: vercel-ai-gateway
6149
6287
  rules: {input_includes_cache_read: true}
6288
+ - provider: vercel
6289
+ alias: minimax/minimax-m3.1-flash
6290
+ channel: vercel-ai-gateway
6291
+ rules: {input_includes_cache_read: true}
6150
6292
  prices:
6151
6293
  - channel: minimax-api
6152
6294
  region: global
@@ -6321,6 +6463,14 @@ models:
6321
6463
  input_includes_cache_write: true
6322
6464
  effective_from: "2026-11-02"
6323
6465
  source_url: https://openrouter.ai/qwen/qwen3-coder-30b-a3b-instruct
6466
+ - provider: openrouter
6467
+ alias: alibaba/qwen3-coder-30b-a3b-instruct
6468
+ channel: openrouter-api
6469
+ effective_from: "2026-11-02"
6470
+ rules:
6471
+ input_includes_cache_read: true
6472
+ input_includes_cache_write: true
6473
+ source_url: https://openrouter.ai/qwen/qwen3-coder-30b-a3b-instruct
6324
6474
  prices:
6325
6475
  - channel: vercel-ai-gateway
6326
6476
  region: global
@@ -12111,11 +12261,22 @@ models:
12111
12261
  - channel: openai-api
12112
12262
  region: global
12113
12263
  effective_from: "2025-06-10"
12264
+ effective_to: "2026-10-29"
12265
+ input_per_mtok: 2
12266
+ output_per_mtok: 8
12267
+ cache_read_per_mtok: 0.50
12268
+ batch_input_per_mtok: 1
12269
+ batch_output_per_mtok: 4
12270
+ source_url: https://developers.openai.com/api/docs/models/o3
12271
+ - channel: openai-api
12272
+ region: global
12273
+ effective_from: "2026-10-29"
12114
12274
  input_per_mtok: 2
12115
12275
  output_per_mtok: 8
12116
12276
  cache_read_per_mtok: 0.50
12117
12277
  batch_input_per_mtok: 1
12118
12278
  batch_output_per_mtok: 4
12279
+ batch_cache_read_per_mtok: 0.25
12119
12280
  source_url: https://developers.openai.com/api/docs/models/o3
12120
12281
  - channel: vercel-ai-gateway
12121
12282
  region: global
@@ -215,7 +215,7 @@ def parse_google(html_text: str, *, duplicates: list[str] | None = None) -> Tabl
215
215
  the one in effect; per-image output is not a token rate. A label may name
216
216
  the media it covers, as in "Input price (text, image, video)"."""
217
217
  parts = page_text(html_text).split("Try it in Google AI Studio")
218
- tier = r"(?:Free of charge \| |Not available \| )?([^|]*\$[^|]*)"
218
+ tier = r"(?:Free of charge \| |Not available \| )?([^|]*\$[^|]*(?:\| \$[^|]*)*)"
219
219
  table: Table = {}
220
220
  for before, block in zip(parts, parts[1:]):
221
221
  ids = _google_ids([c.strip() for c in before.split("|") if c.strip()])
@@ -226,7 +226,12 @@ def parse_google(html_text: str, *, duplicates: list[str] | None = None) -> Tabl
226
226
  if not (inp and out):
227
227
  continue
228
228
  cache = re.search(r"Context caching price[^|]*\| " + tier, block)
229
- output = None if "per image" in out.group(1) else dollars(out.group(1))
229
+ # A scalar output rate cannot express separate text/video/audio prices.
230
+ # Leave it missing so refresh escalates rather than selecting the first
231
+ # modality and overwriting a manually verified catalog rate.
232
+ output_cell = out.group(1)
233
+ modalities = set(re.findall(r"\((text|video|audio)\)", output_cell, re.I))
234
+ output = None if "per image" in output_cell or len(modalities) > 1 else dollars(output_cell)
230
235
  rates = {"input": dollars(inp.group(1)), "output": output, "cache_read": dollars(cache.group(1)) if cache else None}
231
236
  for model_id in ids:
232
237
  _keep_first(table, model_id, {k: v for k, v in rates.items() if v is not None}, duplicates)
@@ -89,16 +89,25 @@ class LoadedCatalog:
89
89
  # Gateways whose amount billing declines per publisher; see Dispute.
90
90
  disputes: tuple[Dispute, ...] = ()
91
91
 
92
- def canonical_model_id(self, provider: str, model_id: str) -> str:
93
- """Resolve a captured provider-native model id to its canonical id,
94
- using the known source ``provider`` to disambiguate names shared across
95
- providers. An unknown provider, unknown id, or non-string input is left
96
- unchanged so none is ever misattributed."""
92
+ def canonical_model_id(self, provider: str, model_id: str, *, at: Any = None) -> str:
93
+ """Resolve a provider's model name, using ``at`` (default now) for migrations.
94
+
95
+ Unknown or ambiguous names are returned unchanged.
96
+ """
97
97
  if not isinstance(model_id, str) or not isinstance(provider, str):
98
98
  return model_id
99
- return self.canonical_ids.get(
100
- (normalize_provider(provider), model_id), model_id
101
- )
99
+ provider = normalize_provider(provider)
100
+ canonical = self.canonical_ids.get((provider, model_id))
101
+ if canonical is not None:
102
+ return canonical
103
+ when = _coerce_datetime(at) if at is not None else datetime.now(timezone.utc)
104
+ matches = {
105
+ alias.canonical_id
106
+ for alias in self.snapshot._aliases.get((provider, model_id.lower()), ())
107
+ if (alias.effective_from is None or alias.effective_from <= when)
108
+ and (alias.effective_to is None or when < alias.effective_to)
109
+ }
110
+ return next(iter(matches)) if len(matches) == 1 else model_id
102
111
 
103
112
  def infer_direct_channel(self, model: Any) -> str | None:
104
113
  """Infer a direct pricing channel only when the catalog is unambiguous."""
@@ -19,13 +19,14 @@ EXECUTION_PROFILES = frozenset({"default", "bedrock"})
19
19
  # "moonshotai", the name its catalog aliases carry ("moonshot" is only an
20
20
  # input alias the catalog normalizes).
21
21
  ROUTE_PROVIDERS = frozenset(
22
- {"anthropic", "bedrock", "fireworks", "moonshotai", "openai", "vercel"}
22
+ {"anthropic", "bedrock", "fireworks", "mistral", "moonshotai", "openai", "vercel"}
23
23
  )
24
24
  ROUTE_PROVIDER_CHANNELS = MappingProxyType(
25
25
  {
26
26
  "anthropic": "anthropic-api",
27
27
  "bedrock": "aws-bedrock",
28
28
  "fireworks": "fireworks-api",
29
+ "mistral": "mistral-api",
29
30
  "moonshotai": "moonshot-api",
30
31
  "openai": "openai-api",
31
32
  "vercel": "vercel-ai-gateway",
@@ -38,6 +39,7 @@ OFFER_GROUP_PROFILES = MappingProxyType(
38
39
  "openai-direct": "default",
39
40
  "anthropic-direct": "default",
40
41
  "moonshot-direct": "default",
42
+ "mistral-direct": "default",
41
43
  "bedrock": "bedrock",
42
44
  }
43
45
  )
@@ -48,6 +50,7 @@ OFFER_GROUP_CREDENTIALS = MappingProxyType(
48
50
  "openai-direct": "OPENAI_API_KEY",
49
51
  "anthropic-direct": "ANTHROPIC_API_KEY",
50
52
  "moonshot-direct": "MOONSHOT_API_KEY",
53
+ "mistral-direct": "MISTRAL_API_KEY",
51
54
  "bedrock": None,
52
55
  }
53
56
  )
@@ -58,6 +61,7 @@ OFFER_GROUP_PROVIDERS = MappingProxyType(
58
61
  "openai-direct": "openai",
59
62
  "anthropic-direct": "anthropic",
60
63
  "moonshot-direct": "moonshotai",
64
+ "mistral-direct": "mistral",
61
65
  "bedrock": "bedrock",
62
66
  }
63
67
  )
@@ -475,7 +479,7 @@ def validate_model_registry(
475
479
  f"{route.id}: model {route.model_id!r} has no pricing channel "
476
480
  f"{route.pricing_channel!r}"
477
481
  )
478
- canonical = catalog.canonical_model_id(route.provider, route.model_id)
482
+ canonical = catalog.canonical_model_id(route.provider, route.model_id, at=validation_time)
479
483
  if canonical == route.model_id:
480
484
  resolved = catalog.snapshot.resolve_price(
481
485
  model=route.model_id,
@@ -52,6 +52,7 @@ class RetrievalCostResult:
52
52
  cost_usd: Decimal | None
53
53
  status: str
54
54
  reasons: tuple[str, ...] = ()
55
+ source_url: str | None = None
55
56
 
56
57
 
57
58
  # The retrieval operation a counted search or fetch is billed as on each
@@ -231,4 +232,4 @@ class RetrievalCatalog:
231
232
  cost = (Decimal(count) * price.per_1k_usd / _THOUSAND).quantize(
232
233
  _COST_QUANTUM, rounding=ROUND_HALF_UP
233
234
  )
234
- return RetrievalCostResult(price.id, cost, "priced", ())
235
+ return RetrievalCostResult(price.id, cost, "priced", (), price.source_url)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Open, effective-dated LLM pricing catalog and deterministic cost calculator for popular models across major providers and gateways, including OpenAI, Anthropic, Google Gemini, AWS Bedrock, OpenRouter, Vercel AI Gateway, and more.
5
5
  Author: Metergraph
6
6
  License-Expression: Apache-2.0
@@ -603,3 +603,33 @@ lifecycles.
603
603
  The model registry version is an ISO date. When more than one registry ships on
604
604
  the same date, append a positive revision such as `2026-09-25.2` so consumers
605
605
  can distinguish the policies while pricing validation still uses that date.
606
+
607
+
608
+ ## Complete call pricing
609
+
610
+ `price_call_from_response` combines token costs with supported retrieval fees.
611
+ `cost_from_response` remains token-only. Retrieval pricing supports Anthropic
612
+ `web_search` and `web_fetch` on `anthropic-api`; other observed scopes are unresolved.
613
+
614
+ ```python
615
+ from metergraph_core import price_call_from_response
616
+
617
+ result = price_call_from_response(
618
+ "anthropic",
619
+ {"type": "message", "model": "claude-sonnet-5-5", "usage": {
620
+ "input_tokens": 1000, "output_tokens": 100,
621
+ "server_tool_use": {"web_search_requests": 2},
622
+ }},
623
+ at="2026-10-20T00:00:00Z",
624
+ )
625
+ assert str(result.cost_usd) == "0.02300000"
626
+ ```
627
+
628
+ `cost_usd` is set only when the call is fully priced. `known_subtotal_usd`
629
+ contains exact components; `estimated_total_usd` separates token fallback
630
+ estimates when all retrieval fees are known. Missing charges are reported in
631
+ `missing_dimensions`. Counts must be nonnegative integers.
632
+
633
+ For normalized usage, use `price_call` with `ModelUsage` and `RetrievalUsage`.
634
+ To compose an existing token result from the same catalog, use `compose_call_cost`.
635
+ `resolve_billing` accepts complete-call results for gateway reconciliation.
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/metergraph_core/__init__.py
5
5
  src/metergraph_core/billing.py
6
+ src/metergraph_core/calls.py
6
7
  src/metergraph_core/catalog.py
7
8
  src/metergraph_core/catalog_delta.py
8
9
  src/metergraph_core/extractors.py