metergraph-core 0.2.27__tar.gz → 0.2.30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {metergraph_core-0.2.27/src/metergraph_core.egg-info → metergraph_core-0.2.30}/PKG-INFO +1 -1
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/pyproject.toml +1 -1
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/billing.py +55 -16
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/catalog.py +85 -4
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/data/prices.yaml +48 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30/src/metergraph_core.egg-info}/PKG-INFO +1 -1
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/MANIFEST.in +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/README.md +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/setup.cfg +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/__init__.py +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/loader.py +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/top_level.txt +0 -0
|
@@ -6,11 +6,49 @@ from typing import Any, Mapping
|
|
|
6
6
|
|
|
7
7
|
from .catalog import CostResult
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class _QualifiedSource:
|
|
11
|
+
"""A gateway whose reported amount this module will bill from.
|
|
12
|
+
|
|
13
|
+
Three things have to line up before an amount is believed, because the field
|
|
14
|
+
carrying one is open: a customer's own client can write to it.
|
|
15
|
+
|
|
16
|
+
* The **gateway**, so an amount is attributed to a system we have checked.
|
|
17
|
+
* The **endpoint**, because a gateway prices its endpoints differently and a
|
|
18
|
+
figure is only interpretable alongside the one that produced it.
|
|
19
|
+
* The **source**, naming the field the amount was read from, so a row cannot
|
|
20
|
+
qualify by claiming a gateway name alone.
|
|
21
|
+
|
|
22
|
+
A gateway absent from this table is not distrusted so much as unverified:
|
|
23
|
+
its amount is still recorded, and the catalog still prices the call, so the
|
|
24
|
+
two can be compared before it is added here.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
gateway: str
|
|
28
|
+
endpoints: frozenset[str]
|
|
29
|
+
cost_source: str
|
|
30
|
+
upstream_cost_source: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Each entry reconciles against the providers' own published rates, including
|
|
34
|
+
# the per-request and per-query charges a token rate cannot express. Portkey was
|
|
35
|
+
# checked across a 46,241-call export: its figure matches published rates for
|
|
36
|
+
# Anthropic, xAI and Perplexity to the cent, and for OpenAI once the per-search
|
|
37
|
+
# charge is counted -- 20,590 of 20,590 rows, exactly.
|
|
38
|
+
_QUALIFIED_SOURCES = (
|
|
39
|
+
_QualifiedSource(
|
|
40
|
+
gateway="openrouter",
|
|
41
|
+
endpoints=frozenset({"chat.completions"}),
|
|
42
|
+
cost_source="openrouter.usage.cost",
|
|
43
|
+
upstream_cost_source=(
|
|
44
|
+
"openrouter.usage.cost_details.upstream_inference_cost"
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
_QualifiedSource(
|
|
48
|
+
gateway="portkey",
|
|
49
|
+
endpoints=frozenset({"chat.completions", "responses"}),
|
|
50
|
+
cost_source="portkey.cost",
|
|
51
|
+
),
|
|
14
52
|
)
|
|
15
53
|
|
|
16
54
|
|
|
@@ -82,11 +120,12 @@ def normalize_gateway_evidence(row: Mapping[str, Any]) -> GatewayBillingEvidence
|
|
|
82
120
|
)
|
|
83
121
|
|
|
84
122
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
and evidence.endpoint
|
|
89
|
-
|
|
123
|
+
def _qualified_source(evidence: GatewayBillingEvidence) -> _QualifiedSource | None:
|
|
124
|
+
"""The entry this row's gateway and endpoint match, if any."""
|
|
125
|
+
for source in _QUALIFIED_SOURCES:
|
|
126
|
+
if evidence.gateway == source.gateway and evidence.endpoint in source.endpoints:
|
|
127
|
+
return source
|
|
128
|
+
return None
|
|
90
129
|
|
|
91
130
|
|
|
92
131
|
def resolve_billing(
|
|
@@ -95,18 +134,18 @@ def resolve_billing(
|
|
|
95
134
|
) -> BillingDecision:
|
|
96
135
|
"""Select effective cost without combining independent reported amounts."""
|
|
97
136
|
|
|
98
|
-
|
|
137
|
+
qualified = _qualified_source(evidence)
|
|
99
138
|
reported_cost = (
|
|
100
139
|
evidence.reported_cost_usd
|
|
101
|
-
if
|
|
102
|
-
and evidence.reported_cost_source ==
|
|
140
|
+
if qualified is not None
|
|
141
|
+
and evidence.reported_cost_source == qualified.cost_source
|
|
103
142
|
else None
|
|
104
143
|
)
|
|
105
144
|
upstream_cost = (
|
|
106
145
|
evidence.reported_upstream_cost_usd
|
|
107
|
-
if
|
|
108
|
-
and
|
|
109
|
-
==
|
|
146
|
+
if qualified is not None
|
|
147
|
+
and qualified.upstream_cost_source is not None
|
|
148
|
+
and evidence.reported_upstream_cost_source == qualified.upstream_cost_source
|
|
110
149
|
else None
|
|
111
150
|
)
|
|
112
151
|
|
|
@@ -196,10 +196,76 @@ def _tokens(value: Any) -> int | None:
|
|
|
196
196
|
return result if result >= 0 else None
|
|
197
197
|
|
|
198
198
|
|
|
199
|
+
def _off_peak_multiplier(rules: Mapping[str, Any], at: datetime) -> Decimal:
|
|
200
|
+
"""The discount a provider applies outside its published peak hours.
|
|
201
|
+
|
|
202
|
+
A row states the peak rate, which is the provider's list price, and the
|
|
203
|
+
window it applies in. A call outside that window is scaled by
|
|
204
|
+
``multiplier``; a row with no window is time-independent.
|
|
205
|
+
"""
|
|
206
|
+
discount = rules.get("off_peak_discount") or {}
|
|
207
|
+
windows = discount.get("peak_hours_utc") or ()
|
|
208
|
+
multiplier = _decimal(discount.get("multiplier"))
|
|
209
|
+
if not windows or multiplier is None:
|
|
210
|
+
return Decimal("1")
|
|
211
|
+
moment = at.astimezone(timezone.utc)
|
|
212
|
+
if discount.get("peak_weekdays_only") and moment.weekday() >= 5:
|
|
213
|
+
return multiplier
|
|
214
|
+
for window in windows:
|
|
215
|
+
try:
|
|
216
|
+
start, end = window
|
|
217
|
+
except (TypeError, ValueError):
|
|
218
|
+
continue
|
|
219
|
+
if _tokens(start) is None or _tokens(end) is None:
|
|
220
|
+
continue
|
|
221
|
+
if int(start) <= moment.hour < int(end):
|
|
222
|
+
return Decimal("1")
|
|
223
|
+
return multiplier
|
|
224
|
+
|
|
225
|
+
# A count that was never given and a count that was given but cannot be read are
|
|
226
|
+
# different facts with opposite billing consequences for cached tokens: absent
|
|
227
|
+
# means there was no cache to discount, unusable means we do not know. `_tokens`
|
|
228
|
+
# answers both with None, which is right for a count a caller may legitimately
|
|
229
|
+
# omit and wrong for one it supplied.
|
|
230
|
+
_UNUSABLE = object()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _token_count(value: Any) -> Any:
|
|
234
|
+
"""A usable token count, ``None`` when none was given, or ``_UNUSABLE`` when
|
|
235
|
+
one was given that cannot be read as a count."""
|
|
236
|
+
if value is None:
|
|
237
|
+
return None
|
|
238
|
+
if isinstance(value, bool):
|
|
239
|
+
return _UNUSABLE
|
|
240
|
+
try:
|
|
241
|
+
result = int(value)
|
|
242
|
+
except (ValueError, TypeError, OverflowError):
|
|
243
|
+
return _UNUSABLE
|
|
244
|
+
return result if result >= 0 else _UNUSABLE
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _cached_count(value: Any, name: str, reasons: list[str]) -> int | None:
|
|
248
|
+
"""The count to bill for one cached-token field, recording why when the
|
|
249
|
+
value supplied cannot be used.
|
|
250
|
+
|
|
251
|
+
Returns ``None`` only when nothing was given, so a caller that omits a field
|
|
252
|
+
keeps the behaviour it had. An unusable value bills as nothing cached, which
|
|
253
|
+
is the same arithmetic as before, but it now says so: the reason turns the
|
|
254
|
+
result ``partial``, and a caller that trusts only a fully priced figure stops
|
|
255
|
+
reading a silent over-bill as a price.
|
|
256
|
+
"""
|
|
257
|
+
count = _token_count(value)
|
|
258
|
+
if count is _UNUSABLE:
|
|
259
|
+
reasons.append(f"unusable_{name}")
|
|
260
|
+
return 0
|
|
261
|
+
return count
|
|
262
|
+
|
|
263
|
+
|
|
199
264
|
def _price_tokens(
|
|
200
265
|
price: Price,
|
|
201
266
|
rules: Mapping[str, Any],
|
|
202
267
|
*,
|
|
268
|
+
at: datetime,
|
|
203
269
|
input_tokens: Any,
|
|
204
270
|
output_tokens: Any,
|
|
205
271
|
cache_read_tokens: Any,
|
|
@@ -214,11 +280,19 @@ def _price_tokens(
|
|
|
214
280
|
reasons: list[str] = []
|
|
215
281
|
input_count = _tokens(input_tokens)
|
|
216
282
|
output_count = _tokens(output_tokens)
|
|
217
|
-
cache_read_count =
|
|
218
|
-
cache_write_5m_count =
|
|
283
|
+
cache_read_count = _cached_count(cache_read_tokens, "cache_read_tokens", reasons) or 0
|
|
284
|
+
cache_write_5m_count = _cached_count(
|
|
285
|
+
cache_write_5m_tokens, "cache_write_5m_tokens", reasons
|
|
286
|
+
)
|
|
219
287
|
if cache_write_5m_count is None:
|
|
220
|
-
|
|
221
|
-
|
|
288
|
+
# Only an absent split falls back to the aggregate. A stated split we
|
|
289
|
+
# cannot read is not an invitation to substitute a different field.
|
|
290
|
+
cache_write_5m_count = (
|
|
291
|
+
_cached_count(cache_write_tokens, "cache_write_tokens", reasons) or 0
|
|
292
|
+
)
|
|
293
|
+
cache_write_1h_count = (
|
|
294
|
+
_cached_count(cache_write_1h_tokens, "cache_write_1h_tokens", reasons) or 0
|
|
295
|
+
)
|
|
222
296
|
cache_write_count = cache_write_5m_count + cache_write_1h_count
|
|
223
297
|
if input_count is None:
|
|
224
298
|
reasons.append("missing_input_tokens")
|
|
@@ -261,6 +335,11 @@ def _price_tokens(
|
|
|
261
335
|
output_multiplier = _decimal(
|
|
262
336
|
long_context.get("output_multiplier")
|
|
263
337
|
) or Decimal("1")
|
|
338
|
+
# Applies to every rate on the row, cache included, and composes with a
|
|
339
|
+
# long-context tier rather than replacing it.
|
|
340
|
+
off_peak = _off_peak_multiplier(rules, at)
|
|
341
|
+
input_multiplier *= off_peak
|
|
342
|
+
output_multiplier *= off_peak
|
|
264
343
|
|
|
265
344
|
cost = Decimal("0")
|
|
266
345
|
if input_rate is None:
|
|
@@ -439,6 +518,7 @@ class CatalogSnapshot:
|
|
|
439
518
|
cost, reasons = _price_tokens(
|
|
440
519
|
price,
|
|
441
520
|
{**price.rules, **alias.rules},
|
|
521
|
+
at=_coerce_datetime(at),
|
|
442
522
|
input_tokens=input_tokens,
|
|
443
523
|
output_tokens=output_tokens,
|
|
444
524
|
cache_read_tokens=cache_read_tokens,
|
|
@@ -486,6 +566,7 @@ class CatalogSnapshot:
|
|
|
486
566
|
cost, reasons = _price_tokens(
|
|
487
567
|
resolved.price,
|
|
488
568
|
resolved.rules,
|
|
569
|
+
at=when,
|
|
489
570
|
input_tokens=input_tokens,
|
|
490
571
|
output_tokens=output_tokens,
|
|
491
572
|
cache_read_tokens=cache_read_tokens,
|
|
@@ -1235,12 +1235,28 @@ models:
|
|
|
1235
1235
|
- channel: deepseek-api
|
|
1236
1236
|
region: global
|
|
1237
1237
|
effective_from: "2026-04-24"
|
|
1238
|
+
effective_to: "2026-09-10T04:00:00+00:00"
|
|
1238
1239
|
input_per_mtok: 0.14
|
|
1239
1240
|
output_per_mtok: 0.28
|
|
1240
1241
|
cache_read_per_mtok: 0.0028
|
|
1241
1242
|
rules:
|
|
1242
1243
|
input_includes_cache_read: true
|
|
1243
1244
|
source_url: https://api-docs.deepseek.com/quick_start/pricing/
|
|
1245
|
+
# This name is retired but still accepted, and billed at the Flash rate.
|
|
1246
|
+
- channel: deepseek-api
|
|
1247
|
+
region: global
|
|
1248
|
+
effective_from: "2026-09-10T04:00:00+00:00"
|
|
1249
|
+
input_per_mtok: 0.30
|
|
1250
|
+
output_per_mtok: 1.20
|
|
1251
|
+
cache_read_per_mtok: 0.006
|
|
1252
|
+
rules:
|
|
1253
|
+
input_includes_cache_read: true
|
|
1254
|
+
# Peak rate; halved outside the window. Footnote (3) on the source.
|
|
1255
|
+
off_peak_discount:
|
|
1256
|
+
multiplier: 0.5
|
|
1257
|
+
peak_hours_utc: [[1, 4], [6, 10]]
|
|
1258
|
+
peak_weekdays_only: true
|
|
1259
|
+
source_url: https://api-docs.deepseek.com/quick_start/pricing/
|
|
1244
1260
|
- channel: vercel-ai-gateway
|
|
1245
1261
|
region: global
|
|
1246
1262
|
effective_from: "2026-04-24"
|
|
@@ -1274,12 +1290,27 @@ models:
|
|
|
1274
1290
|
- channel: deepseek-api
|
|
1275
1291
|
region: global
|
|
1276
1292
|
effective_from: "2026-04-24"
|
|
1293
|
+
effective_to: "2026-09-10T04:00:00+00:00"
|
|
1277
1294
|
input_per_mtok: 0.435
|
|
1278
1295
|
output_per_mtok: 0.87
|
|
1279
1296
|
cache_read_per_mtok: 0.003625
|
|
1280
1297
|
rules:
|
|
1281
1298
|
input_includes_cache_read: true
|
|
1282
1299
|
source_url: https://api-docs.deepseek.com/quick_start/pricing/
|
|
1300
|
+
- channel: deepseek-api
|
|
1301
|
+
region: global
|
|
1302
|
+
effective_from: "2026-09-10T04:00:00+00:00"
|
|
1303
|
+
input_per_mtok: 1.32
|
|
1304
|
+
output_per_mtok: 3.96
|
|
1305
|
+
cache_read_per_mtok: 0.044
|
|
1306
|
+
rules:
|
|
1307
|
+
input_includes_cache_read: true
|
|
1308
|
+
# Peak rate; halved outside the window. Footnote (3) on the source.
|
|
1309
|
+
off_peak_discount:
|
|
1310
|
+
multiplier: 0.5
|
|
1311
|
+
peak_hours_utc: [[1, 4], [6, 10]]
|
|
1312
|
+
peak_weekdays_only: true
|
|
1313
|
+
source_url: https://api-docs.deepseek.com/quick_start/pricing/
|
|
1283
1314
|
- channel: vercel-ai-gateway
|
|
1284
1315
|
region: global
|
|
1285
1316
|
effective_from: "2026-04-24"
|
|
@@ -1581,11 +1612,28 @@ models:
|
|
|
1581
1612
|
- canonical_id: deepseek/deepseek-v4.1-flash
|
|
1582
1613
|
publisher: deepseek
|
|
1583
1614
|
aliases:
|
|
1615
|
+
- {provider: deepseek, alias: deepseek-flash, channel: deepseek-api}
|
|
1616
|
+
- {provider: litellm, alias: deepseek-flash, channel: deepseek-api}
|
|
1617
|
+
- {provider: unknown, alias: deepseek-flash, channel: deepseek-api}
|
|
1584
1618
|
- provider: deepseek
|
|
1585
1619
|
alias: deepseek/deepseek-v4.1-flash
|
|
1586
1620
|
channel: vercel-ai-gateway
|
|
1587
1621
|
rules: {input_includes_cache_read: true}
|
|
1588
1622
|
prices:
|
|
1623
|
+
- channel: deepseek-api
|
|
1624
|
+
region: global
|
|
1625
|
+
effective_from: "2026-09-10T04:00:00+00:00"
|
|
1626
|
+
input_per_mtok: 0.30
|
|
1627
|
+
output_per_mtok: 1.20
|
|
1628
|
+
cache_read_per_mtok: 0.006
|
|
1629
|
+
rules:
|
|
1630
|
+
input_includes_cache_read: true
|
|
1631
|
+
# Peak rate; halved outside the window. Footnote (3) on the source.
|
|
1632
|
+
off_peak_discount:
|
|
1633
|
+
multiplier: 0.5
|
|
1634
|
+
peak_hours_utc: [[1, 4], [6, 10]]
|
|
1635
|
+
peak_weekdays_only: true
|
|
1636
|
+
source_url: https://api-docs.deepseek.com/quick_start/pricing/
|
|
1589
1637
|
- channel: vercel-ai-gateway
|
|
1590
1638
|
region: global
|
|
1591
1639
|
effective_from: "2026-09-08"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.27 → metergraph_core-0.2.30}/src/metergraph_core.egg-info/top_level.txt
RENAMED
|
File without changes
|