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.
- {metergraph_core-0.7.6/src/metergraph_core.egg-info → metergraph_core-0.8.1}/PKG-INFO +6 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/README.md +5 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/pyproject.toml +1 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/__init__.py +2 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/catalog.py +190 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/models.yaml +46 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/loader.py +12 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/models.py +10 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1/src/metergraph_core.egg-info}/PKG-INFO +6 -1
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/MANIFEST.in +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/setup.cfg +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/billing.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/catalog_delta.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/prices.yaml +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/data/sources.yaml +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/extractors.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/refresh.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/responses.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core/sources.py +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.7.6 → metergraph_core-0.8.1}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|