metergraph-core 0.7.6__tar.gz → 0.8.1__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 (24) hide show
  1. {metergraph_core-0.7.6/src/metergraph_core.egg-info → metergraph_core-0.8.1}/PKG-INFO +6 -1
  2. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/README.md +5 -0
  3. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/pyproject.toml +1 -1
  4. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/__init__.py +2 -0
  5. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/catalog.py +190 -0
  6. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/models.yaml +46 -1
  7. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/loader.py +12 -1
  8. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/models.py +10 -1
  9. {metergraph_core-0.7.6 → metergraph_core-0.8.1/src/metergraph_core.egg-info}/PKG-INFO +6 -1
  10. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/MANIFEST.in +0 -0
  11. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/setup.cfg +0 -0
  12. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/billing.py +0 -0
  13. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/catalog_delta.py +0 -0
  14. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/prices.yaml +0 -0
  15. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/sources.yaml +0 -0
  16. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/extractors.py +0 -0
  17. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/refresh.py +0 -0
  18. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/responses.py +0 -0
  19. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/retrieval.py +0 -0
  20. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/sources.py +0 -0
  21. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
  22. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
  23. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/requires.txt +0 -0
  24. {metergraph_core-0.7.6 → metergraph_core-0.8.1}/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.7.6
3
+ Version: 0.8.1
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
@@ -210,6 +210,11 @@ A call after it ended resolves to that price, stays `priced`, and carries
210
210
  applied `price.effective_to` says when it ended. A closed price followed by a
211
211
  later one never extends, so a gap before a scheduled price stays `unpriced`.
212
212
 
213
+ An `unpriced` result whose cause is a value you passed, such as an unknown
214
+ channel, provider or model, says why in `warnings`. Each `PricingWarning` has
215
+ a stable `code` and a `message` that may suggest the value you meant. Match on
216
+ `code`; the message text may change.
217
+
213
218
  ### Costing a provider response
214
219
 
215
220
  `cost_from_response` reads the usage out of a response you already have and
@@ -180,6 +180,11 @@ A call after it ended resolves to that price, stays `priced`, and carries
180
180
  applied `price.effective_to` says when it ended. A closed price followed by a
181
181
  later one never extends, so a gap before a scheduled price stays `unpriced`.
182
182
 
183
+ An `unpriced` result whose cause is a value you passed, such as an unknown
184
+ channel, provider or model, says why in `warnings`. Each `PricingWarning` has
185
+ a stable `code` and a `message` that may suggest the value you meant. Match on
186
+ `code`; the message text may change.
187
+
183
188
  ### Costing a provider response
184
189
 
185
190
  `cost_from_response` reads the usage out of a response you already have and
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "metergraph-core"
3
- version = "0.7.6"
3
+ version = "0.8.1"
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"
@@ -8,6 +8,7 @@ from .catalog import (
8
8
  ModelUsage,
9
9
  Price,
10
10
  PriceResult,
11
+ PricingWarning,
11
12
  ResolvedPrice,
12
13
  counts_cache_read_in_input,
13
14
  direct_channel_for_provider,
@@ -128,6 +129,7 @@ __all__ = [
128
129
  "GatewayBillingEvidence",
129
130
  "Price",
130
131
  "PriceResult",
132
+ "PricingWarning",
131
133
  "ResolvedPrice",
132
134
  "RetrievalCatalog",
133
135
  "RetrievalCostResult",
@@ -1,5 +1,6 @@
1
1
  """Effective-dated model catalog and deterministic token-cost enrichment."""
2
2
 
3
+ import difflib
3
4
  import re
4
5
  from dataclasses import dataclass
5
6
  from datetime import date, datetime, timezone
@@ -86,6 +87,16 @@ _GLOBAL_REGION = "global"
86
87
  # Reason for a result priced at a price that ended and was not replaced.
87
88
  PRICE_ENDED = "price_ended"
88
89
 
90
+ # Warning codes: why a result is unpriced, when the cause is a value the
91
+ # caller sent. The codes are stable; the messages may change.
92
+ UNKNOWN_CHANNEL = "unknown_channel"
93
+ UNKNOWN_PROVIDER = "unknown_provider"
94
+ UNKNOWN_MODEL = "unknown_model"
95
+ MODEL_NOT_ON_CHANNEL = "model_not_on_channel"
96
+ MODEL_NOT_IN_EFFECT = "model_not_in_effect"
97
+ MODEL_NOT_IN_REGION = "model_not_in_region"
98
+ PRICE_NOT_IN_EFFECT = "price_not_in_effect"
99
+
89
100
 
90
101
  def _bedrock_fallback_ids(model_key: str) -> tuple[tuple[str, bool], ...]:
91
102
  """Candidate ids for an unmatched Bedrock model, most specific first.
@@ -301,6 +312,9 @@ class CostResult:
301
312
  # Inputs the caller did not supply that the figure depends on: "region",
302
313
  # "channel", "input_tokens", "output_tokens".
303
314
  missing_dimensions: tuple[str, ...] = ()
315
+ # Why an unpriced result is unpriced, when the cause is a value the
316
+ # caller sent. Empty otherwise.
317
+ warnings: tuple["PricingWarning", ...] = ()
304
318
 
305
319
  @property
306
320
  def model_id(self) -> str | None:
@@ -343,6 +357,30 @@ class PriceResult:
343
357
  missing_dimensions: tuple[str, ...] = ()
344
358
  # Notes that do not make the result partial, such as ``price_ended``.
345
359
  reasons: tuple[str, ...] = ()
360
+ # Why an unpriced result is unpriced, when the cause is a value the
361
+ # caller sent. Empty otherwise.
362
+ warnings: tuple["PricingWarning", ...] = ()
363
+
364
+
365
+ @dataclass(frozen=True, slots=True)
366
+ class PricingWarning:
367
+ """Why a result is unpriced, when the cause is a value the caller sent:
368
+ a channel or provider the catalog cannot bill on, a model name it does
369
+ not know, or a model the channel does not sell at that time. ``code`` is
370
+ stable; ``message`` names the value and, when one is close, the value
371
+ the caller probably meant."""
372
+
373
+ code: str
374
+ message: str
375
+
376
+
377
+ def _did_you_mean(
378
+ value: str, options: Any, guess: str | None = None, cutoff: float = 0.6
379
+ ) -> str:
380
+ options = sorted(options)
381
+ close = difflib.get_close_matches(value, options, 1, cutoff)
382
+ match = guess if guess in options else next(iter(close), None)
383
+ return f' Did you mean "{match}"?' if match else ""
346
384
 
347
385
 
348
386
  @dataclass(frozen=True, slots=True)
@@ -713,6 +751,17 @@ class CatalogSnapshot:
713
751
  )
714
752
  for candidates in self._prices.values():
715
753
  candidates.sort(key=lambda price: price.effective_from, reverse=True)
754
+ # For explaining an unpriced result: the channels some price row
755
+ # bills on, and each model name with the channels that sell it under
756
+ # that name, an alias there and a price there, at any time.
757
+ priced = {(model_id, channel.lower()) for model_id, channel in self._prices}
758
+ self._priced_channels = frozenset(channel for _, channel in priced)
759
+ sold: dict[str, set[str]] = {}
760
+ for (model, channel), candidates in self._deployment_aliases.items():
761
+ found = sold.setdefault(model, set())
762
+ if any((alias.model_id, channel) in priced for alias in candidates):
763
+ found.add(channel)
764
+ self._sold = {model: frozenset(channels) for model, channels in sold.items()}
716
765
  # Every named region a price or pin exists for: the regions an omitted
717
766
  # query region could have meant.
718
767
  self._regions = tuple(sorted(
@@ -894,6 +943,147 @@ class CatalogSnapshot:
894
943
  return resolved, ("region",)
895
944
  return resolved, ()
896
945
 
946
+ def _explain(
947
+ self, *, model: Any, channel: Any, provider: Any, at: Any, region: Any
948
+ ) -> tuple[PricingWarning, ...]:
949
+ """Why :meth:`_lookup` found no price, when the cause is a value the
950
+ caller sent. Each check follows the step of the lookup it explains:
951
+ the channel, or the provider's direct channel, then the model name on
952
+ that channel, then the alias and price windows at ``at``."""
953
+ channel_key = str(channel or "").strip().lower()
954
+ if channel_key:
955
+ if channel_key not in self._priced_channels:
956
+ # A provider's name, with or without "-api", points at its
957
+ # direct channel.
958
+ guess = direct_channel_for_provider(
959
+ channel_key
960
+ ) or direct_channel_for_provider(channel_key.removesuffix("-api"))
961
+ return (PricingWarning(
962
+ UNKNOWN_CHANNEL,
963
+ f'channel "{str(channel).strip()}" is not a channel the catalog prices.'
964
+ f"{_did_you_mean(channel_key, self._priced_channels, guess)}",
965
+ ),)
966
+ else:
967
+ if provider is None or not str(provider).strip():
968
+ return ()
969
+ channel_key = direct_channel_for_provider(provider)
970
+ if channel_key is None:
971
+ providers = set(_DIRECT_CHANNEL_BY_PROVIDER) | {
972
+ spelling for spelling, name in _PROVIDER_ALIASES.items()
973
+ if name in _DIRECT_CHANNEL_BY_PROVIDER
974
+ }
975
+ return (PricingWarning(
976
+ UNKNOWN_PROVIDER,
977
+ f'provider "{str(provider).strip()}" has no billing channel of its own.'
978
+ f"{_did_you_mean(str(provider).strip().lower(), providers)} If a gateway or "
979
+ "proxy relayed the call, name the provider that served it or the channel "
980
+ "that billed it.",
981
+ ),)
982
+ model_key = str(model or "").strip().lower()
983
+ if not model_key:
984
+ return ()
985
+ # Bedrock fallback ids, under the rules _resolve applies: dropping
986
+ # "global." is valid only on aws-bedrock, and never across a price
987
+ # region.
988
+ keys, dropped, forbidden = [model_key], set(), []
989
+ if channel_key in _BEDROCK_CHANNELS:
990
+ for candidate, dropped_global in _bedrock_fallback_ids(model_key):
991
+ if dropped_global and channel_key != _BEDROCK_CHANNEL:
992
+ forbidden.append(candidate)
993
+ continue
994
+ keys.append(candidate)
995
+ if dropped_global:
996
+ dropped.add(candidate)
997
+ name = str(model).strip()
998
+ if not any(key in self._sold for key in keys):
999
+ # A "global." id on a geo channel points at the bare id that
1000
+ # channel sells.
1001
+ guess = next(
1002
+ (key for key in forbidden if channel_key in self._sold.get(key, ())), None
1003
+ )
1004
+ return (PricingWarning(
1005
+ UNKNOWN_MODEL,
1006
+ f'model "{name}" is not in the catalog.'
1007
+ f"{_did_you_mean(model_key, self._sold, guess, cutoff=0.8)}",
1008
+ ),)
1009
+ candidates = tuple(
1010
+ alias
1011
+ for key in keys
1012
+ for alias in self._deployment_aliases.get((key, channel_key), ())
1013
+ if key not in dropped or not _crosses_price_region(alias)
1014
+ )
1015
+ sold = sorted({channel for key in keys for channel in self._sold.get(key, ())})
1016
+ if not candidates or channel_key not in sold:
1017
+ where = (
1018
+ f"It is priced on {', '.join(sold)}."
1019
+ if sold
1020
+ else "No channel prices it under that name."
1021
+ )
1022
+ return (PricingWarning(
1023
+ MODEL_NOT_ON_CHANNEL,
1024
+ f'model "{name}" has no price on channel "{channel_key}". {where}',
1025
+ ),)
1026
+ when = _coerce_datetime(at)
1027
+ stated = str(region or "").strip().lower() or None
1028
+ alias = self._alias_for(candidates, when, stated)
1029
+ if alias is None:
1030
+ return (self._unresolved(name, channel_key, candidates, when, stated),)
1031
+ price, _ = self._price_for(alias, when, stated)
1032
+ if price is None:
1033
+ scope = f' in region "{stated}"' if stated else ""
1034
+ return (PricingWarning(
1035
+ PRICE_NOT_IN_EFFECT,
1036
+ f'model "{name}" has no price on channel "{channel_key}"{scope} in effect at '
1037
+ f"{when.isoformat()}.",
1038
+ ),)
1039
+ return ()
1040
+
1041
+ def _unresolved(
1042
+ self,
1043
+ name: str,
1044
+ channel: str,
1045
+ candidates: tuple[Alias, ...],
1046
+ when: datetime,
1047
+ region: str | None,
1048
+ ) -> PricingWarning:
1049
+ """Why no alias for ``name`` on ``channel`` applies at ``when``: its
1050
+ windows close before or open after it, or the aliases in effect are
1051
+ pinned to other regions."""
1052
+ current = [
1053
+ alias for alias in candidates
1054
+ if (alias.effective_from is None or alias.effective_from <= when)
1055
+ and (alias.effective_to is None or when < alias.effective_to)
1056
+ ]
1057
+ if current:
1058
+ regions = sorted({pin for alias in current for pin in alias.price_region})
1059
+ asked = region or self._region
1060
+ return PricingWarning(
1061
+ MODEL_NOT_IN_REGION,
1062
+ f'model "{name}" resolves on channel "{channel}" only in '
1063
+ f"{', '.join(regions)}, not {asked}.",
1064
+ )
1065
+ ended = [alias.effective_to for alias in candidates
1066
+ if alias.effective_to is not None and alias.effective_to <= when]
1067
+ starts = [alias.effective_from for alias in candidates
1068
+ if alias.effective_from is not None and when < alias.effective_from]
1069
+ if ended and starts:
1070
+ when_text = (
1071
+ f"between {max(ended).date().isoformat()} and "
1072
+ f"{min(starts).date().isoformat()}"
1073
+ )
1074
+ message = f'model "{name}" does not resolve on channel "{channel}" {when_text}.'
1075
+ elif ended:
1076
+ message = (
1077
+ f'model "{name}" stopped resolving on channel "{channel}" on '
1078
+ f"{max(ended).date().isoformat()}."
1079
+ )
1080
+ else:
1081
+ message = (
1082
+ f'model "{name}" resolves on channel "{channel}" from '
1083
+ f"{min(starts).date().isoformat()}."
1084
+ )
1085
+ return PricingWarning(MODEL_NOT_IN_EFFECT, message)
1086
+
897
1087
  def cost(
898
1088
  self,
899
1089
  *,
@@ -1,4 +1,4 @@
1
- version: '2026-10-19'
1
+ version: '2026-10-27'
2
2
  models:
3
3
  - canonical_id: openai/gpt-5.6-sol
4
4
  display_name: GPT-5.6 Sol
@@ -852,6 +852,42 @@ models:
852
852
  pricing_channel: vercel-ai-gateway
853
853
  execution_profiles:
854
854
  - default
855
+ - canonical_id: openai/gpt-6.1-terra
856
+ display_name: GPT-6.1 Terra
857
+ publisher: openai
858
+ routes:
859
+ - key: default:openai:gpt-6.1-terra
860
+ id: openai:gpt-6.1-terra
861
+ provider: openai
862
+ model_id: gpt-6.1-terra
863
+ pricing_channel: openai-api
864
+ execution_profiles:
865
+ - default
866
+ display_name: GPT-6.1 Terra
867
+ - canonical_id: moonshotai/kimi-k3.1-pro
868
+ display_name: Kimi K3.1 Pro
869
+ publisher: moonshotai
870
+ routes:
871
+ - key: default:moonshotai:kimi-k3.1-pro
872
+ id: moonshotai:kimi-k3.1-pro
873
+ provider: moonshotai
874
+ model_id: kimi-k3.1-pro
875
+ pricing_channel: moonshot-api
876
+ execution_profiles:
877
+ - default
878
+ display_name: Kimi K3.1 Pro (direct)
879
+ - canonical_id: moonshotai/kimi-k3.1-pro-highspeed
880
+ display_name: Kimi K3.1 Pro Highspeed
881
+ publisher: moonshotai
882
+ routes:
883
+ - key: default:moonshotai:kimi-k3.1-pro-highspeed
884
+ id: moonshotai:kimi-k3.1-pro-highspeed
885
+ provider: moonshotai
886
+ model_id: kimi-k3.1-pro-highspeed
887
+ pricing_channel: moonshot-api
888
+ execution_profiles:
889
+ - default
890
+ display_name: Kimi K3.1 Pro Highspeed (direct)
855
891
  execution_profiles:
856
892
  - id: default
857
893
  routes:
@@ -924,6 +960,9 @@ execution_profiles:
924
960
  - default:openai:gpt-6.1-sol
925
961
  - default:anthropic:claude-sonnet-5-5
926
962
  - default:anthropic:claude-haiku-5-5
963
+ - default:openai:gpt-6.1-terra
964
+ - default:moonshotai:kimi-k3.1-pro
965
+ - default:moonshotai:kimi-k3.1-pro-highspeed
927
966
  - id: bedrock
928
967
  routes:
929
968
  - bedrock:anthropic/claude-sonnet-5
@@ -1005,6 +1044,7 @@ offer_groups:
1005
1044
  - default:openai:gpt-5.4-mini
1006
1045
  - default:openai:gpt-5.4-nano
1007
1046
  - default:openai:gpt-6.1-sol
1047
+ - default:openai:gpt-6.1-terra
1008
1048
  - id: anthropic-direct
1009
1049
  credential: ANTHROPIC_API_KEY
1010
1050
  routes:
@@ -1014,6 +1054,11 @@ offer_groups:
1014
1054
  - default:anthropic:claude-haiku-4-5
1015
1055
  - default:anthropic:claude-sonnet-5-5
1016
1056
  - default:anthropic:claude-haiku-5-5
1057
+ - id: moonshot-direct
1058
+ credential: MOONSHOT_API_KEY
1059
+ routes:
1060
+ - default:moonshotai:kimi-k3.1-pro
1061
+ - default:moonshotai:kimi-k3.1-pro-highspeed
1017
1062
  - id: bedrock
1018
1063
  credential: null
1019
1064
  routes:
@@ -153,7 +153,8 @@ class LoadedCatalog:
153
153
  model=model, channel=channel, provider=provider, at=when, region=region
154
154
  )
155
155
  if resolved is None:
156
- return PriceResult("unpriced", None, None, when, missing)
156
+ warnings = self._explain(model, channel, provider, when, region, missing)
157
+ return PriceResult("unpriced", None, None, when, missing, warnings=warnings)
157
158
  return PriceResult(
158
159
  "partial" if missing else "priced",
159
160
  resolved.canonical_model,
@@ -163,6 +164,15 @@ class LoadedCatalog:
163
164
  (PRICE_ENDED,) if resolved.ended else (),
164
165
  )
165
166
 
167
+ def _explain(self, model, channel, provider, at, region, missing):
168
+ """Warnings for an unpriced result. A missing region already says
169
+ why, so it gets none."""
170
+ if "region" in missing:
171
+ return ()
172
+ return self.snapshot._explain(
173
+ model=model, channel=channel, provider=provider, at=at, region=region
174
+ )
175
+
166
176
  def calculate_cost(
167
177
  self,
168
178
  *,
@@ -190,6 +200,7 @@ class LoadedCatalog:
190
200
  reasons or ("unknown_deployment",),
191
201
  effective_at=when,
192
202
  missing_dimensions=missing,
203
+ warnings=self._explain(model, channel, provider, when, region, missing),
193
204
  )
194
205
  return _cost_result(resolved, at=when, usage=usage, missing=missing)
195
206
 
@@ -15,12 +15,18 @@ from .loader import LoadedCatalog
15
15
 
16
16
 
17
17
  EXECUTION_PROFILES = frozenset({"default", "bedrock"})
18
- ROUTE_PROVIDERS = frozenset({"anthropic", "bedrock", "fireworks", "openai", "vercel"})
18
+ # Route providers use the catalog's normalized provider names: Moonshot is
19
+ # "moonshotai", the name its catalog aliases carry ("moonshot" is only an
20
+ # input alias the catalog normalizes).
21
+ ROUTE_PROVIDERS = frozenset(
22
+ {"anthropic", "bedrock", "fireworks", "moonshotai", "openai", "vercel"}
23
+ )
19
24
  ROUTE_PROVIDER_CHANNELS = MappingProxyType(
20
25
  {
21
26
  "anthropic": "anthropic-api",
22
27
  "bedrock": "aws-bedrock",
23
28
  "fireworks": "fireworks-api",
29
+ "moonshotai": "moonshot-api",
24
30
  "openai": "openai-api",
25
31
  "vercel": "vercel-ai-gateway",
26
32
  }
@@ -31,6 +37,7 @@ OFFER_GROUP_PROFILES = MappingProxyType(
31
37
  "fireworks": "default",
32
38
  "openai-direct": "default",
33
39
  "anthropic-direct": "default",
40
+ "moonshot-direct": "default",
34
41
  "bedrock": "bedrock",
35
42
  }
36
43
  )
@@ -40,6 +47,7 @@ OFFER_GROUP_CREDENTIALS = MappingProxyType(
40
47
  "fireworks": "FIREWORKS_API_KEY",
41
48
  "openai-direct": "OPENAI_API_KEY",
42
49
  "anthropic-direct": "ANTHROPIC_API_KEY",
50
+ "moonshot-direct": "MOONSHOT_API_KEY",
43
51
  "bedrock": None,
44
52
  }
45
53
  )
@@ -49,6 +57,7 @@ OFFER_GROUP_PROVIDERS = MappingProxyType(
49
57
  "fireworks": "fireworks",
50
58
  "openai-direct": "openai",
51
59
  "anthropic-direct": "anthropic",
60
+ "moonshot-direct": "moonshotai",
52
61
  "bedrock": "bedrock",
53
62
  }
54
63
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: metergraph-core
3
- Version: 0.7.6
3
+ Version: 0.8.1
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
@@ -210,6 +210,11 @@ A call after it ended resolves to that price, stays `priced`, and carries
210
210
  applied `price.effective_to` says when it ended. A closed price followed by a
211
211
  later one never extends, so a gap before a scheduled price stays `unpriced`.
212
212
 
213
+ An `unpriced` result whose cause is a value you passed, such as an unknown
214
+ channel, provider or model, says why in `warnings`. Each `PricingWarning` has
215
+ a stable `code` and a `message` that may suggest the value you meant. Match on
216
+ `code`; the message text may change.
217
+
213
218
  ### Costing a provider response
214
219
 
215
220
  `cost_from_response` reads the usage out of a response you already have and