metergraph-core 0.2.52__tar.gz → 0.2.54__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.52/src/metergraph_core.egg-info → metergraph_core-0.2.54}/PKG-INFO +1 -1
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/pyproject.toml +1 -1
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/__init__.py +6 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/billing.py +60 -5
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/catalog.py +44 -11
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/data/models.yaml +27 -1
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/data/prices.yaml +15 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/loader.py +42 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54/src/metergraph_core.egg-info}/PKG-INFO +1 -1
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/MANIFEST.in +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/README.md +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/setup.cfg +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/models.py +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Reusable MeterGraph catalog and deterministic token-cost pricing core."""
|
|
2
2
|
|
|
3
3
|
from .catalog import (
|
|
4
|
+
CostComponents,
|
|
4
5
|
Alias,
|
|
5
6
|
CatalogSnapshot,
|
|
6
7
|
CostResult,
|
|
@@ -11,12 +12,14 @@ from .catalog import (
|
|
|
11
12
|
normalize_provider,
|
|
12
13
|
)
|
|
13
14
|
from .billing import (
|
|
15
|
+
Dispute,
|
|
14
16
|
BillingDecision,
|
|
15
17
|
GatewayBillingEvidence,
|
|
16
18
|
normalize_gateway_evidence,
|
|
17
19
|
resolve_billing,
|
|
18
20
|
)
|
|
19
21
|
from .loader import (
|
|
22
|
+
parse_disputes,
|
|
20
23
|
CatalogError,
|
|
21
24
|
LoadedCatalog,
|
|
22
25
|
load_catalog,
|
|
@@ -44,7 +47,9 @@ __all__ = [
|
|
|
44
47
|
"BillingDecision",
|
|
45
48
|
"CatalogError",
|
|
46
49
|
"CatalogSnapshot",
|
|
50
|
+
"CostComponents",
|
|
47
51
|
"CostResult",
|
|
52
|
+
"Dispute",
|
|
48
53
|
"LoadedCatalog",
|
|
49
54
|
"ModelDefinition",
|
|
50
55
|
"ModelRegistry",
|
|
@@ -65,6 +70,7 @@ __all__ = [
|
|
|
65
70
|
"normalize_gateway_evidence",
|
|
66
71
|
"parse_catalog",
|
|
67
72
|
"parse_model_registry",
|
|
73
|
+
"parse_disputes",
|
|
68
74
|
"parse_retrieval",
|
|
69
75
|
"resolve_billing",
|
|
70
76
|
"validate_model_registry",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Pure selection of effective LLM cost from catalog and gateway evidence."""
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from decimal import Decimal, InvalidOperation
|
|
5
|
-
from typing import Any, Mapping
|
|
6
|
+
from typing import Any, Iterable, Mapping
|
|
6
7
|
|
|
7
|
-
from .catalog import CostResult
|
|
8
|
+
from .catalog import CostResult, _coerce_datetime
|
|
8
9
|
|
|
9
10
|
@dataclass(frozen=True, slots=True)
|
|
10
11
|
class _QualifiedSource:
|
|
@@ -56,6 +57,20 @@ _QUALIFIED_SOURCES = (
|
|
|
56
57
|
)
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class Dispute:
|
|
62
|
+
"""A gateway whose reported amount is not believed for one publisher's
|
|
63
|
+
models from a date: the catalog has shown its figure wrong there, and the
|
|
64
|
+
note says how. Billing takes the catalog price instead and keeps the
|
|
65
|
+
gateway's figure as evidence."""
|
|
66
|
+
|
|
67
|
+
gateway: str
|
|
68
|
+
publisher: str
|
|
69
|
+
since: datetime
|
|
70
|
+
note: str
|
|
71
|
+
source_url: str
|
|
72
|
+
|
|
73
|
+
|
|
59
74
|
@dataclass(frozen=True, slots=True)
|
|
60
75
|
class GatewayBillingEvidence:
|
|
61
76
|
"""Validated gateway billing fields from one content-blind call row."""
|
|
@@ -132,11 +147,43 @@ def _qualified_source(evidence: GatewayBillingEvidence) -> _QualifiedSource | No
|
|
|
132
147
|
return None
|
|
133
148
|
|
|
134
149
|
|
|
150
|
+
def _disputed(
|
|
151
|
+
qualified: _QualifiedSource | None,
|
|
152
|
+
publisher: str | None,
|
|
153
|
+
disputes: Iterable[Dispute],
|
|
154
|
+
at: Any,
|
|
155
|
+
) -> bool:
|
|
156
|
+
# Without the call time nothing is disputed: a dispute runs from a date,
|
|
157
|
+
# and declining a figure for a call that may predate it is the wrong
|
|
158
|
+
# side to err on.
|
|
159
|
+
if qualified is None or publisher is None or at is None:
|
|
160
|
+
return False
|
|
161
|
+
when = _coerce_datetime(at)
|
|
162
|
+
return any(
|
|
163
|
+
dispute.gateway == qualified.gateway
|
|
164
|
+
and dispute.publisher == publisher
|
|
165
|
+
and dispute.since <= when
|
|
166
|
+
for dispute in disputes
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
135
170
|
def resolve_billing(
|
|
136
171
|
catalog_result: CostResult,
|
|
137
172
|
evidence: GatewayBillingEvidence,
|
|
173
|
+
*,
|
|
174
|
+
disputes: Iterable[Dispute] = (),
|
|
175
|
+
at: Any = None,
|
|
138
176
|
) -> BillingDecision:
|
|
139
|
-
"""Select effective cost without combining independent reported amounts.
|
|
177
|
+
"""Select effective cost without combining independent reported amounts.
|
|
178
|
+
|
|
179
|
+
A qualified gateway's amount is declined when the catalog disputes that
|
|
180
|
+
gateway for the priced model's publisher at ``at`` (with no ``at`` nothing
|
|
181
|
+
is disputed): the catalog price is
|
|
182
|
+
billed, the gateway's figure stays in ``reported_cost_usd`` as evidence,
|
|
183
|
+
and ``gateway_disputed`` is recorded among the reasons. When the catalog
|
|
184
|
+
cannot price the call, the gateway's figure still stands, with the same
|
|
185
|
+
reason recorded, because a disputed figure is better evidence than none.
|
|
186
|
+
"""
|
|
140
187
|
|
|
141
188
|
qualified = _qualified_source(evidence)
|
|
142
189
|
reported_cost = (
|
|
@@ -145,6 +192,12 @@ def resolve_billing(
|
|
|
145
192
|
and evidence.reported_cost_source == qualified.cost_source
|
|
146
193
|
else None
|
|
147
194
|
)
|
|
195
|
+
disputed = reported_cost is not None and _disputed(
|
|
196
|
+
qualified, catalog_result.publisher, disputes, at
|
|
197
|
+
)
|
|
198
|
+
reasons = catalog_result.reasons
|
|
199
|
+
if disputed:
|
|
200
|
+
reasons = tuple(dict.fromkeys((*reasons, "gateway_disputed")))
|
|
148
201
|
upstream_cost = (
|
|
149
202
|
evidence.reported_upstream_cost_usd
|
|
150
203
|
if qualified is not None
|
|
@@ -153,7 +206,9 @@ def resolve_billing(
|
|
|
153
206
|
else None
|
|
154
207
|
)
|
|
155
208
|
|
|
156
|
-
if reported_cost is not None
|
|
209
|
+
if reported_cost is not None and not (
|
|
210
|
+
disputed and catalog_result.cost_usd is not None
|
|
211
|
+
):
|
|
157
212
|
cost_usd = reported_cost
|
|
158
213
|
cost_status = "priced"
|
|
159
214
|
provenance = "gateway_reported"
|
|
@@ -174,5 +229,5 @@ def resolve_billing(
|
|
|
174
229
|
reported_upstream_cost_usd=upstream_cost,
|
|
175
230
|
catalog_cost_usd=catalog_result.cost_usd,
|
|
176
231
|
catalog_price_id=catalog_result.price_id,
|
|
177
|
-
catalog_reasons=
|
|
232
|
+
catalog_reasons=reasons,
|
|
178
233
|
)
|
|
@@ -211,6 +211,19 @@ class Price:
|
|
|
211
211
|
publisher: str | None = None
|
|
212
212
|
|
|
213
213
|
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class CostComponents:
|
|
216
|
+
"""A priced call's cost by what it paid for. The four parts sum to the
|
|
217
|
+
call's ``cost_usd`` exactly: each is quantized and the rounding residue
|
|
218
|
+
lands in the input part, so a consumer that stores them can rebuild the
|
|
219
|
+
total without a second rounding."""
|
|
220
|
+
|
|
221
|
+
input_usd: Decimal
|
|
222
|
+
output_usd: Decimal
|
|
223
|
+
cache_read_usd: Decimal
|
|
224
|
+
cache_write_usd: Decimal
|
|
225
|
+
|
|
226
|
+
|
|
214
227
|
@dataclass(frozen=True, slots=True)
|
|
215
228
|
class CostResult:
|
|
216
229
|
canonical_model: str | None
|
|
@@ -218,6 +231,11 @@ class CostResult:
|
|
|
218
231
|
cost_usd: Decimal | None
|
|
219
232
|
status: str
|
|
220
233
|
reasons: tuple[str, ...] = ()
|
|
234
|
+
# Present whenever cost_usd is: the split of that cost by driver.
|
|
235
|
+
components: CostComponents | None = None
|
|
236
|
+
# The catalog's publisher for the priced model, for billing rules that
|
|
237
|
+
# apply per publisher rather than per channel.
|
|
238
|
+
publisher: str | None = None
|
|
221
239
|
|
|
222
240
|
|
|
223
241
|
@dataclass(frozen=True, slots=True)
|
|
@@ -326,10 +344,11 @@ def _price_tokens(
|
|
|
326
344
|
cache_write_5m_tokens: Any,
|
|
327
345
|
cache_write_1h_tokens: Any,
|
|
328
346
|
batch: bool,
|
|
329
|
-
) -> tuple[Decimal, list[str]]:
|
|
347
|
+
) -> tuple[Decimal, list[str], CostComponents]:
|
|
330
348
|
"""Cost a token usage against one already-selected price and its merged
|
|
331
349
|
rules. Shared by ``cost`` (provider+model entry) and ``price_deployment``
|
|
332
|
-
(model+channel entry) so both compute identically.
|
|
350
|
+
(model+channel entry) so both compute identically. Returns the total, the
|
|
351
|
+
reasons it is partial, and the split by driver."""
|
|
333
352
|
reasons: list[str] = []
|
|
334
353
|
input_count = _tokens(input_tokens)
|
|
335
354
|
output_count = _tokens(output_tokens)
|
|
@@ -394,22 +413,22 @@ def _price_tokens(
|
|
|
394
413
|
input_multiplier *= off_peak
|
|
395
414
|
output_multiplier *= off_peak
|
|
396
415
|
|
|
397
|
-
|
|
416
|
+
input_usd = output_usd = cache_read_usd = cache_write_usd = Decimal("0")
|
|
398
417
|
if input_rate is None:
|
|
399
418
|
if billable_input:
|
|
400
419
|
reasons.append("input_rate_unavailable")
|
|
401
420
|
else:
|
|
402
|
-
|
|
421
|
+
input_usd = Decimal(billable_input) * input_rate * input_multiplier / _MILLION
|
|
403
422
|
if output_rate is None:
|
|
404
423
|
if output_count:
|
|
405
424
|
reasons.append("output_rate_unavailable")
|
|
406
425
|
else:
|
|
407
|
-
|
|
426
|
+
output_usd = Decimal(output_count) * output_rate * output_multiplier / _MILLION
|
|
408
427
|
if cache_read_count:
|
|
409
428
|
if price.cache_read_per_mtok is None:
|
|
410
429
|
reasons.append("cache_read_rate_unavailable")
|
|
411
430
|
else:
|
|
412
|
-
|
|
431
|
+
cache_read_usd = (
|
|
413
432
|
Decimal(cache_read_count)
|
|
414
433
|
* price.cache_read_per_mtok
|
|
415
434
|
* input_multiplier
|
|
@@ -419,7 +438,7 @@ def _price_tokens(
|
|
|
419
438
|
if price.cache_write_5m_per_mtok is None:
|
|
420
439
|
reasons.append("cache_write_5m_rate_unavailable")
|
|
421
440
|
else:
|
|
422
|
-
|
|
441
|
+
cache_write_usd += (
|
|
423
442
|
Decimal(cache_write_5m_count)
|
|
424
443
|
* price.cache_write_5m_per_mtok
|
|
425
444
|
* input_multiplier
|
|
@@ -429,7 +448,7 @@ def _price_tokens(
|
|
|
429
448
|
if price.cache_write_1h_per_mtok is None:
|
|
430
449
|
reasons.append("cache_write_1h_rate_unavailable")
|
|
431
450
|
else:
|
|
432
|
-
|
|
451
|
+
cache_write_usd += (
|
|
433
452
|
Decimal(cache_write_1h_count)
|
|
434
453
|
* price.cache_write_1h_per_mtok
|
|
435
454
|
* input_multiplier
|
|
@@ -438,7 +457,17 @@ def _price_tokens(
|
|
|
438
457
|
if rules.get("uncaptured_fees"):
|
|
439
458
|
reasons.append("uncaptured_fees")
|
|
440
459
|
|
|
441
|
-
|
|
460
|
+
# The total is rounded once, as it always was; the parts are rounded
|
|
461
|
+
# each and the residue goes to input, so they sum to the total exactly.
|
|
462
|
+
cost = (input_usd + output_usd + cache_read_usd + cache_write_usd).quantize(
|
|
463
|
+
_COST_QUANTUM, rounding=ROUND_HALF_UP
|
|
464
|
+
)
|
|
465
|
+
parts = [
|
|
466
|
+
part.quantize(_COST_QUANTUM, rounding=ROUND_HALF_UP)
|
|
467
|
+
for part in (input_usd, output_usd, cache_read_usd, cache_write_usd)
|
|
468
|
+
]
|
|
469
|
+
parts[0] += cost - sum(parts)
|
|
470
|
+
return cost, reasons, CostComponents(*parts)
|
|
442
471
|
|
|
443
472
|
|
|
444
473
|
class CatalogSnapshot:
|
|
@@ -641,7 +670,7 @@ class CatalogSnapshot:
|
|
|
641
670
|
("no_effective_price",),
|
|
642
671
|
)
|
|
643
672
|
|
|
644
|
-
cost, reasons = _price_tokens(
|
|
673
|
+
cost, reasons, components = _price_tokens(
|
|
645
674
|
price,
|
|
646
675
|
{**price.rules, **alias.rules},
|
|
647
676
|
at=when,
|
|
@@ -659,6 +688,8 @@ class CatalogSnapshot:
|
|
|
659
688
|
cost,
|
|
660
689
|
"partial" if reasons else "priced",
|
|
661
690
|
tuple(dict.fromkeys(reasons)),
|
|
691
|
+
components=components,
|
|
692
|
+
publisher=price.publisher,
|
|
662
693
|
)
|
|
663
694
|
|
|
664
695
|
def price_deployment(
|
|
@@ -689,7 +720,7 @@ class CatalogSnapshot:
|
|
|
689
720
|
resolved = self.resolve_price(model=model, channel=channel, at=when)
|
|
690
721
|
if resolved is None:
|
|
691
722
|
return CostResult(None, None, None, "unpriced", ("unknown_deployment",))
|
|
692
|
-
cost, reasons = _price_tokens(
|
|
723
|
+
cost, reasons, components = _price_tokens(
|
|
693
724
|
resolved.price,
|
|
694
725
|
resolved.rules,
|
|
695
726
|
at=when,
|
|
@@ -707,4 +738,6 @@ class CatalogSnapshot:
|
|
|
707
738
|
cost,
|
|
708
739
|
"partial" if reasons else "priced",
|
|
709
740
|
tuple(dict.fromkeys(reasons)),
|
|
741
|
+
components=components,
|
|
742
|
+
publisher=resolved.price.publisher,
|
|
710
743
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
version: '2026-
|
|
1
|
+
version: '2026-10-06'
|
|
2
2
|
models:
|
|
3
3
|
- canonical_id: openai/gpt-5.6-sol
|
|
4
4
|
display_name: GPT-5.6 Sol
|
|
@@ -581,6 +581,28 @@ models:
|
|
|
581
581
|
pricing_channel: aws-bedrock
|
|
582
582
|
execution_profiles:
|
|
583
583
|
- bedrock
|
|
584
|
+
- canonical_id: anthropic/claude-opus-5.5
|
|
585
|
+
display_name: Claude Opus 5.5
|
|
586
|
+
publisher: anthropic
|
|
587
|
+
routes:
|
|
588
|
+
- key: default:anthropic/claude-opus-5.5
|
|
589
|
+
id: anthropic/claude-opus-5.5
|
|
590
|
+
provider: vercel
|
|
591
|
+
model_id: anthropic/claude-opus-5.5
|
|
592
|
+
pricing_channel: vercel-ai-gateway
|
|
593
|
+
execution_profiles:
|
|
594
|
+
- default
|
|
595
|
+
- canonical_id: anthropic/claude-sonnet-5.5
|
|
596
|
+
display_name: Claude Sonnet 5.5
|
|
597
|
+
publisher: anthropic
|
|
598
|
+
routes:
|
|
599
|
+
- key: default:anthropic/claude-sonnet-5.5
|
|
600
|
+
id: anthropic/claude-sonnet-5.5
|
|
601
|
+
provider: vercel
|
|
602
|
+
model_id: anthropic/claude-sonnet-5.5
|
|
603
|
+
pricing_channel: vercel-ai-gateway
|
|
604
|
+
execution_profiles:
|
|
605
|
+
- default
|
|
584
606
|
execution_profiles:
|
|
585
607
|
- id: default
|
|
586
608
|
routes:
|
|
@@ -617,6 +639,8 @@ execution_profiles:
|
|
|
617
639
|
- default:deepseek/deepseek-v4.1-flash
|
|
618
640
|
- default:alibaba/qwen3.8-flash
|
|
619
641
|
- default:inception/mercury-2.5
|
|
642
|
+
- default:anthropic/claude-opus-5.5
|
|
643
|
+
- default:anthropic/claude-sonnet-5.5
|
|
620
644
|
- default:openai/gpt-5.4-mini
|
|
621
645
|
- default:openai/gpt-5.4-nano
|
|
622
646
|
- default:anthropic/claude-opus-4.8
|
|
@@ -684,6 +708,8 @@ offer_groups:
|
|
|
684
708
|
- default:deepseek/deepseek-v4.1-flash
|
|
685
709
|
- default:alibaba/qwen3.8-flash
|
|
686
710
|
- default:inception/mercury-2.5
|
|
711
|
+
- default:anthropic/claude-opus-5.5
|
|
712
|
+
- default:anthropic/claude-sonnet-5.5
|
|
687
713
|
- id: fireworks
|
|
688
714
|
credential: FIREWORKS_API_KEY
|
|
689
715
|
routes:
|
|
@@ -1876,6 +1876,8 @@ models:
|
|
|
1876
1876
|
- <<: *deepseek-v4-flash-retired
|
|
1877
1877
|
provider: unknown
|
|
1878
1878
|
alias: deepseek/deepseek-v4-flash
|
|
1879
|
+
- <<: *deepseek-v4-flash-retired
|
|
1880
|
+
alias: deepseek/deepseek/deepseek-v4-flash
|
|
1879
1881
|
- &deepseek-chat-v4-flash
|
|
1880
1882
|
{provider: deepseek, alias: deepseek-chat, channel: deepseek-api, effective_from: "2026-04-24", effective_to: "2026-07-24T16:00:00+00:00", source_url: https://api-docs.deepseek.com/news/news260424/}
|
|
1881
1883
|
- <<: *deepseek-chat-v4-flash
|
|
@@ -2450,6 +2452,8 @@ models:
|
|
|
2450
2452
|
- <<: *deepseek-v4-flash-routed
|
|
2451
2453
|
provider: unknown
|
|
2452
2454
|
alias: deepseek/deepseek-v4-flash
|
|
2455
|
+
- <<: *deepseek-v4-flash-routed
|
|
2456
|
+
alias: deepseek/deepseek/deepseek-v4-flash
|
|
2453
2457
|
- &deepseek-v4-flash-vision-routed
|
|
2454
2458
|
{provider: deepseek, alias: deepseek-v4-flash-vision-exp, channel: deepseek-api, effective_from: "2026-09-10T04:00:00+00:00", source_url: https://api-docs.deepseek.com/updates/}
|
|
2455
2459
|
- <<: *deepseek-v4-flash-vision-routed
|
|
@@ -4286,3 +4290,14 @@ retrieval:
|
|
|
4286
4290
|
per_1k_usd: 12.00
|
|
4287
4291
|
effective_from: "2025-04-18"
|
|
4288
4292
|
source_url: https://docs.perplexity.ai/docs/getting-started/pricing
|
|
4293
|
+
|
|
4294
|
+
# Gateways whose reported amount billing declines for one publisher's models
|
|
4295
|
+
# from a date, because the catalog has shown the gateway's figure wrong there.
|
|
4296
|
+
# The call is billed at the catalog price and the gateway's figure is kept as
|
|
4297
|
+
# evidence; a product shows the note beside the figure.
|
|
4298
|
+
disputes:
|
|
4299
|
+
- gateway: portkey
|
|
4300
|
+
publisher: deepseek
|
|
4301
|
+
since: "2026-09-10"
|
|
4302
|
+
note: Portkey prices DeepSeek from a table that predates DeepSeek's 2026-09-10 price change, at a quarter to a half of the list price.
|
|
4303
|
+
source_url: https://api-docs.deepseek.com/updates/
|
|
@@ -18,6 +18,7 @@ from .catalog import (
|
|
|
18
18
|
_effective_windows_overlap,
|
|
19
19
|
normalize_provider,
|
|
20
20
|
)
|
|
21
|
+
from .billing import Dispute
|
|
21
22
|
from .retrieval import RetrievalCatalog, RetrievalCostResult, RetrievalPrice
|
|
22
23
|
|
|
23
24
|
DEFAULT_CATALOG_PATH = Path(__file__).parent / "data" / "prices.yaml"
|
|
@@ -75,6 +76,8 @@ class LoadedCatalog:
|
|
|
75
76
|
snapshot: CatalogSnapshot
|
|
76
77
|
canonical_ids: Mapping[tuple[str, str], str]
|
|
77
78
|
retrieval: RetrievalCatalog
|
|
79
|
+
# Gateways whose amount billing declines per publisher; see Dispute.
|
|
80
|
+
disputes: tuple[Dispute, ...] = ()
|
|
78
81
|
|
|
79
82
|
def canonical_model_id(self, provider: str, model_id: str) -> str:
|
|
80
83
|
"""Resolve a captured provider-native model id to its canonical id,
|
|
@@ -385,6 +388,44 @@ def parse_retrieval(document: Any) -> list[RetrievalPrice]:
|
|
|
385
388
|
return prices
|
|
386
389
|
|
|
387
390
|
|
|
391
|
+
def parse_disputes(document: Any) -> list[Dispute]:
|
|
392
|
+
"""Parse the optional top-level ``disputes`` list: a gateway whose
|
|
393
|
+
reported amount is not believed for one publisher's models from a date,
|
|
394
|
+
with the note a product can show and the page that shows the figure is
|
|
395
|
+
wrong. An absent key yields no disputes."""
|
|
396
|
+
if not isinstance(document, dict):
|
|
397
|
+
raise CatalogError("prices document must be a mapping")
|
|
398
|
+
entries = document.get("disputes")
|
|
399
|
+
if entries is None:
|
|
400
|
+
return []
|
|
401
|
+
if not isinstance(entries, list):
|
|
402
|
+
raise CatalogError("prices document disputes must be a list")
|
|
403
|
+
disputes: list[Dispute] = []
|
|
404
|
+
for entry in entries:
|
|
405
|
+
if not isinstance(entry, dict):
|
|
406
|
+
raise CatalogError("dispute entry must be a mapping")
|
|
407
|
+
gateway = str(entry.get("gateway") or "").strip().lower()
|
|
408
|
+
publisher = str(entry.get("publisher") or "").strip().lower()
|
|
409
|
+
if not gateway or not publisher:
|
|
410
|
+
raise CatalogError("dispute entry needs gateway/publisher")
|
|
411
|
+
label = f"{gateway}/{publisher}"
|
|
412
|
+
note = str(entry.get("note") or "").strip()
|
|
413
|
+
if not note:
|
|
414
|
+
raise CatalogError(f"{label}: dispute entry needs a note")
|
|
415
|
+
if not str(entry.get("source_url") or "").strip():
|
|
416
|
+
raise CatalogError(f"{label}: dispute entry needs source_url")
|
|
417
|
+
disputes.append(
|
|
418
|
+
Dispute(
|
|
419
|
+
gateway=gateway,
|
|
420
|
+
publisher=publisher,
|
|
421
|
+
since=_date(entry.get("since"), field="since", model=label),
|
|
422
|
+
note=note,
|
|
423
|
+
source_url=str(entry["source_url"]).strip(),
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
return disputes
|
|
427
|
+
|
|
428
|
+
|
|
388
429
|
def load_catalog(
|
|
389
430
|
path: str | Path | None = None, *, region: str = "global"
|
|
390
431
|
) -> LoadedCatalog:
|
|
@@ -402,4 +443,5 @@ def load_catalog(
|
|
|
402
443
|
snapshot=CatalogSnapshot(aliases, prices, region=region),
|
|
403
444
|
canonical_ids=_canonical_index(document),
|
|
404
445
|
retrieval=RetrievalCatalog(parse_retrieval(document)),
|
|
446
|
+
disputes=tuple(parse_disputes(document)),
|
|
405
447
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{metergraph_core-0.2.52 → metergraph_core-0.2.54}/src/metergraph_core.egg-info/top_level.txt
RENAMED
|
File without changes
|