metergraph-core 0.8.1__tar.gz → 0.8.2__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.8.1/src/metergraph_core.egg-info → metergraph_core-0.8.2}/PKG-INFO +36 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/README.md +35 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/pyproject.toml +1 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/catalog_delta.py +157 -17
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/data/prices.yaml +15 -4
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/refresh.py +56 -14
- {metergraph_core-0.8.1 → metergraph_core-0.8.2/src/metergraph_core.egg-info}/PKG-INFO +36 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/MANIFEST.in +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/setup.cfg +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/__init__.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/billing.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/catalog.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/data/models.yaml +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/data/sources.yaml +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/extractors.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/loader.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/models.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/responses.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core/sources.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.2}/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.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -356,6 +356,41 @@ effective_from)` on a model the catalog already has:
|
|
|
356
356
|
start on a later UTC date than the row it replaces, for the same reason.
|
|
357
357
|
- `close_price` sets `effective_to` on the open row without a replacement.
|
|
358
358
|
|
|
359
|
+
Catalog operations change which models exist and what calls they match:
|
|
360
|
+
|
|
361
|
+
- `add_model` appends one complete model entry.
|
|
362
|
+
- `add_alias` appends one alias to an existing model. A spelling another
|
|
363
|
+
model already holds is a conflict only while both aliases are in effect.
|
|
364
|
+
- `close_alias` sets `effective_to` on an open alias, named by
|
|
365
|
+
`(canonical_id, provider, alias, channel)`; `expect` is the exact current
|
|
366
|
+
alias. An alias with dates must cite its source, so a close carries
|
|
367
|
+
`source_url` when the alias has none and must not when it has one.
|
|
368
|
+
|
|
369
|
+
A `close_alias`, `close_price`, `add_alias` and `add_price` together move a
|
|
370
|
+
spelling from a duplicate entry to the model it duplicates from a date. Calls
|
|
371
|
+
before that date keep the old model and price, calls from it resolve to the
|
|
372
|
+
surviving model, and the old entry stays for history with nothing in effect:
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
- op: close_alias
|
|
376
|
+
key: {canonical_id: example-host/model-a-instruct, provider: openrouter,
|
|
377
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
378
|
+
expect: {provider: openrouter, alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
379
|
+
effective_to: "2026-10-20"
|
|
380
|
+
source_url: "https://..."
|
|
381
|
+
- op: close_price # the duplicate's open openrouter-api row
|
|
382
|
+
- op: add_alias
|
|
383
|
+
key: {canonical_id: example/model-a, provider: openrouter,
|
|
384
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
385
|
+
expect: null
|
|
386
|
+
alias: {provider: openrouter, alias: example-host/model-a-instruct,
|
|
387
|
+
channel: openrouter-api, effective_from: "2026-10-20", source_url: "https://..."}
|
|
388
|
+
- op: add_price # the same rate on example/model-a from 2026-10-20
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Gateway discovery matches a spelling only through aliases in effect on the
|
|
392
|
+
observation date, so it follows a moved spelling to its new model.
|
|
393
|
+
|
|
359
394
|
A price id ends with the UTC date of its row's start. If two rows for one
|
|
360
395
|
model, channel and region would still share an id (possible in a hand-edited
|
|
361
396
|
catalog), each that does not start at UTC midnight also names its UTC start
|
|
@@ -326,6 +326,41 @@ effective_from)` on a model the catalog already has:
|
|
|
326
326
|
start on a later UTC date than the row it replaces, for the same reason.
|
|
327
327
|
- `close_price` sets `effective_to` on the open row without a replacement.
|
|
328
328
|
|
|
329
|
+
Catalog operations change which models exist and what calls they match:
|
|
330
|
+
|
|
331
|
+
- `add_model` appends one complete model entry.
|
|
332
|
+
- `add_alias` appends one alias to an existing model. A spelling another
|
|
333
|
+
model already holds is a conflict only while both aliases are in effect.
|
|
334
|
+
- `close_alias` sets `effective_to` on an open alias, named by
|
|
335
|
+
`(canonical_id, provider, alias, channel)`; `expect` is the exact current
|
|
336
|
+
alias. An alias with dates must cite its source, so a close carries
|
|
337
|
+
`source_url` when the alias has none and must not when it has one.
|
|
338
|
+
|
|
339
|
+
A `close_alias`, `close_price`, `add_alias` and `add_price` together move a
|
|
340
|
+
spelling from a duplicate entry to the model it duplicates from a date. Calls
|
|
341
|
+
before that date keep the old model and price, calls from it resolve to the
|
|
342
|
+
surviving model, and the old entry stays for history with nothing in effect:
|
|
343
|
+
|
|
344
|
+
```yaml
|
|
345
|
+
- op: close_alias
|
|
346
|
+
key: {canonical_id: example-host/model-a-instruct, provider: openrouter,
|
|
347
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
348
|
+
expect: {provider: openrouter, alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
349
|
+
effective_to: "2026-10-20"
|
|
350
|
+
source_url: "https://..."
|
|
351
|
+
- op: close_price # the duplicate's open openrouter-api row
|
|
352
|
+
- op: add_alias
|
|
353
|
+
key: {canonical_id: example/model-a, provider: openrouter,
|
|
354
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
355
|
+
expect: null
|
|
356
|
+
alias: {provider: openrouter, alias: example-host/model-a-instruct,
|
|
357
|
+
channel: openrouter-api, effective_from: "2026-10-20", source_url: "https://..."}
|
|
358
|
+
- op: add_price # the same rate on example/model-a from 2026-10-20
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Gateway discovery matches a spelling only through aliases in effect on the
|
|
362
|
+
observation date, so it follows a moved spelling to its new model.
|
|
363
|
+
|
|
329
364
|
A price id ends with the UTC date of its row's start. If two rows for one
|
|
330
365
|
model, channel and region would still share an id (possible in a hand-edited
|
|
331
366
|
catalog), each that does not start at UTC midnight also names its UTC start
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "metergraph-core"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.2"
|
|
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"
|
|
@@ -15,7 +15,15 @@ catalog already has:
|
|
|
15
15
|
It also supports reviewed catalog expansion:
|
|
16
16
|
|
|
17
17
|
- ``add_model`` appends one complete model entry.
|
|
18
|
-
- ``add_alias`` appends one alias to an existing model.
|
|
18
|
+
- ``add_alias`` appends one alias to an existing model. A spelling another
|
|
19
|
+
model holds conflicts only while both aliases are in effect, so a spelling
|
|
20
|
+
can move to another model from a date.
|
|
21
|
+
- ``close_alias`` sets ``effective_to`` on the open alias its ``expect``
|
|
22
|
+
names, so a spelling closed and reopened on one model keeps its earlier
|
|
23
|
+
window. With an
|
|
24
|
+
``add_alias`` for the same spelling on another model in one delta, it
|
|
25
|
+
moves that spelling: calls before the date keep the old model, calls from
|
|
26
|
+
it resolve to the new one.
|
|
19
27
|
|
|
20
28
|
``provenance`` references the observations and evidence the delta came
|
|
21
29
|
from.
|
|
@@ -34,7 +42,7 @@ from .loader import CatalogError, _canonical_json, load_catalog_document
|
|
|
34
42
|
CATALOG_DELTA_SCHEMA_VERSION = 2
|
|
35
43
|
PRICING_DELTA_KIND = "pricing_delta"
|
|
36
44
|
PRICE_OPERATIONS = ("add_price", "replace_price", "close_price")
|
|
37
|
-
CATALOG_OPERATIONS = ("add_model", "add_alias")
|
|
45
|
+
CATALOG_OPERATIONS = ("add_model", "add_alias", "close_alias")
|
|
38
46
|
OPERATIONS = PRICE_OPERATIONS + CATALOG_OPERATIONS
|
|
39
47
|
|
|
40
48
|
RATE_FIELDS = (
|
|
@@ -116,12 +124,14 @@ class DeltaOperation:
|
|
|
116
124
|
expect: Mapping[str, Any] | None
|
|
117
125
|
# The new row for add/replace; None for close.
|
|
118
126
|
price: Mapping[str, Any] | None
|
|
119
|
-
# The closing date for close_price.
|
|
127
|
+
# The closing date for close_price and close_alias.
|
|
120
128
|
effective_to: str | None
|
|
121
129
|
# The complete entry for add_model.
|
|
122
130
|
model: Mapping[str, Any] | None = None
|
|
123
131
|
# The alias row for add_alias.
|
|
124
132
|
alias: Mapping[str, Any] | None = None
|
|
133
|
+
# The source a close_alias cites, for an alias that cites none.
|
|
134
|
+
source_url: str | None = None
|
|
125
135
|
|
|
126
136
|
|
|
127
137
|
@dataclass(frozen=True, slots=True)
|
|
@@ -197,7 +207,7 @@ def _parse_model_key(raw: Any, label: str) -> ModelKey:
|
|
|
197
207
|
return ModelKey(_text(raw["canonical_id"]))
|
|
198
208
|
|
|
199
209
|
|
|
200
|
-
def _parse_alias_key(raw: Any, label: str) -> AliasKey:
|
|
210
|
+
def _parse_alias_key(raw: Any, label: str, op: str) -> AliasKey:
|
|
201
211
|
if not isinstance(raw, Mapping):
|
|
202
212
|
raise CatalogDeltaError(f"{label}: key must be a mapping")
|
|
203
213
|
fields = ("canonical_id", "provider", "alias", "channel")
|
|
@@ -205,7 +215,7 @@ def _parse_alias_key(raw: Any, label: str) -> AliasKey:
|
|
|
205
215
|
if _text(raw.get(field)) is None:
|
|
206
216
|
raise CatalogDeltaError(f"{label}: key needs {field}")
|
|
207
217
|
if set(raw) != set(fields):
|
|
208
|
-
raise CatalogDeltaError(f"{label}:
|
|
218
|
+
raise CatalogDeltaError(f"{label}: {op} key has unknown fields")
|
|
209
219
|
return AliasKey(
|
|
210
220
|
_text(raw["canonical_id"]),
|
|
211
221
|
_text(raw["provider"]).lower(),
|
|
@@ -324,8 +334,8 @@ def _parse_operation(index: int, raw: Any) -> DeltaOperation:
|
|
|
324
334
|
raise CatalogDeltaError(f"{label}: unknown op {op!r}")
|
|
325
335
|
if op == "add_model":
|
|
326
336
|
key: OperationKey = _parse_model_key(raw.get("key"), label)
|
|
327
|
-
elif op
|
|
328
|
-
key = _parse_alias_key(raw.get("key"), label)
|
|
337
|
+
elif op in ("add_alias", "close_alias"):
|
|
338
|
+
key = _parse_alias_key(raw.get("key"), label, op)
|
|
329
339
|
else:
|
|
330
340
|
key = _parse_key(raw.get("key"), label)
|
|
331
341
|
expect = raw.get("expect")
|
|
@@ -338,6 +348,7 @@ def _parse_operation(index: int, raw: Any) -> DeltaOperation:
|
|
|
338
348
|
effective_to = None
|
|
339
349
|
model = None
|
|
340
350
|
alias = None
|
|
351
|
+
source_url = None
|
|
341
352
|
if op == "add_model":
|
|
342
353
|
if "price" in raw or "alias" in raw or "effective_to" in raw:
|
|
343
354
|
raise CatalogDeltaError(f"{label}: add_model takes only model")
|
|
@@ -346,6 +357,18 @@ def _parse_operation(index: int, raw: Any) -> DeltaOperation:
|
|
|
346
357
|
if "price" in raw or "model" in raw or "effective_to" in raw:
|
|
347
358
|
raise CatalogDeltaError(f"{label}: add_alias takes only alias")
|
|
348
359
|
alias = _parse_alias(raw.get("alias"), key, label)
|
|
360
|
+
elif op == "close_alias":
|
|
361
|
+
if set(raw) - {"op", "key", "expect", "effective_to", "source_url"}:
|
|
362
|
+
raise CatalogDeltaError(
|
|
363
|
+
f"{label}: close_alias takes only effective_to and source_url"
|
|
364
|
+
)
|
|
365
|
+
effective_to = _iso_date(raw.get("effective_to"))
|
|
366
|
+
if effective_to is None:
|
|
367
|
+
raise CatalogDeltaError(f"{label}: invalid effective_to")
|
|
368
|
+
if "source_url" in raw:
|
|
369
|
+
source_url = _text(raw.get("source_url"))
|
|
370
|
+
if source_url is None:
|
|
371
|
+
raise CatalogDeltaError(f"{label}: invalid source_url")
|
|
349
372
|
elif op == "close_price":
|
|
350
373
|
if "price" in raw:
|
|
351
374
|
raise CatalogDeltaError(f"{label}: close_price takes no price")
|
|
@@ -369,6 +392,7 @@ def _parse_operation(index: int, raw: Any) -> DeltaOperation:
|
|
|
369
392
|
effective_to=effective_to,
|
|
370
393
|
model=model,
|
|
371
394
|
alias=alias,
|
|
395
|
+
source_url=source_url,
|
|
372
396
|
)
|
|
373
397
|
|
|
374
398
|
|
|
@@ -480,8 +504,53 @@ def _key_label(key: OperationKey) -> str:
|
|
|
480
504
|
return key.canonical_id
|
|
481
505
|
|
|
482
506
|
|
|
483
|
-
def
|
|
484
|
-
|
|
507
|
+
def _alias_rows(
|
|
508
|
+
entry: Mapping[str, Any], key: AliasKey
|
|
509
|
+
) -> list[Mapping[str, Any]]:
|
|
510
|
+
return [
|
|
511
|
+
alias
|
|
512
|
+
for alias in entry.get("aliases") or []
|
|
513
|
+
if isinstance(alias, Mapping)
|
|
514
|
+
and (_text(alias.get("provider")) or "").lower() == key.provider
|
|
515
|
+
and (_text(alias.get("alias")) or "").lower() == key.alias
|
|
516
|
+
and _text(alias.get("channel")) == key.channel
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _expected_aliases(
|
|
521
|
+
entry: Mapping[str, Any], operation: DeltaOperation
|
|
522
|
+
) -> list[Mapping[str, Any]]:
|
|
523
|
+
"""The rows under a close_alias key that equal its ``expect``. A key can
|
|
524
|
+
hold several windows of one spelling; ``expect`` names the one to end."""
|
|
525
|
+
expected = _canonical_json(operation.expect)
|
|
526
|
+
return [
|
|
527
|
+
alias
|
|
528
|
+
for alias in _alias_rows(entry, operation.key)
|
|
529
|
+
if _canonical_json(alias) == expected
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _windows_overlap(
|
|
534
|
+
left_from: Any, left_to: Any, right_from: Any, right_to: Any
|
|
535
|
+
) -> bool:
|
|
536
|
+
"""Whether two alias windows share an instant; a missing bound is open,
|
|
537
|
+
as the catalog loader reads it."""
|
|
538
|
+
left_from, left_to = _instant(left_from), _instant(left_to)
|
|
539
|
+
right_from, right_to = _instant(right_from), _instant(right_to)
|
|
540
|
+
return (left_to is None or right_from is None or right_from < left_to) and (
|
|
541
|
+
right_to is None or left_from is None or left_from < right_to
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _precondition(
|
|
546
|
+
document: Mapping[str, Any],
|
|
547
|
+
operation: DeltaOperation,
|
|
548
|
+
closing: Mapping[AliasKey, str] | None = None,
|
|
549
|
+
) -> str | None:
|
|
550
|
+
"""Why ``operation`` does not apply to ``document``, or None when it does.
|
|
551
|
+
|
|
552
|
+
``closing`` maps the key of each alias a close_alias in the same delta
|
|
553
|
+
ends to its ``effective_to``, so an alias can move in one delta."""
|
|
485
554
|
if operation.op == "add_model":
|
|
486
555
|
key = operation.key
|
|
487
556
|
if _model_indexes(document, key.canonical_id):
|
|
@@ -502,13 +571,69 @@ def _precondition(document: Mapping[str, Any], operation: DeltaOperation) -> str
|
|
|
502
571
|
return "unknown_model"
|
|
503
572
|
if len(models) > 1:
|
|
504
573
|
return "duplicate_model"
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
574
|
+
closing = closing or {}
|
|
575
|
+
new = operation.alias or {}
|
|
576
|
+
for entry in document.get("models") or []:
|
|
577
|
+
if not isinstance(entry, Mapping):
|
|
578
|
+
continue
|
|
579
|
+
canonical_id = str(entry.get("canonical_id") or "")
|
|
580
|
+
for alias in entry.get("aliases") or []:
|
|
581
|
+
if not isinstance(alias, Mapping):
|
|
582
|
+
continue
|
|
583
|
+
provider = (_text(alias.get("provider")) or "").lower()
|
|
584
|
+
name = (_text(alias.get("alias")) or "").lower()
|
|
585
|
+
if (provider, name) != (key.provider, key.alias):
|
|
586
|
+
continue
|
|
587
|
+
ends = alias.get("effective_to")
|
|
588
|
+
if ends is None:
|
|
589
|
+
ends = closing.get(
|
|
590
|
+
AliasKey(
|
|
591
|
+
canonical_id,
|
|
592
|
+
provider,
|
|
593
|
+
name,
|
|
594
|
+
_text(alias.get("channel")) or "",
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
if _windows_overlap(
|
|
598
|
+
alias.get("effective_from"),
|
|
599
|
+
ends,
|
|
600
|
+
new.get("effective_from"),
|
|
601
|
+
new.get("effective_to"),
|
|
602
|
+
):
|
|
603
|
+
return (
|
|
604
|
+
"alias_exists"
|
|
605
|
+
if canonical_id == key.canonical_id
|
|
606
|
+
else "alias_conflict"
|
|
607
|
+
)
|
|
608
|
+
return None
|
|
609
|
+
if operation.op == "close_alias":
|
|
610
|
+
key = operation.key
|
|
611
|
+
models = _model_indexes(document, key.canonical_id)
|
|
612
|
+
if not models:
|
|
613
|
+
return "unknown_model"
|
|
614
|
+
if len(models) > 1:
|
|
615
|
+
return "duplicate_model"
|
|
616
|
+
entry = document["models"][models[0]]
|
|
617
|
+
if not _alias_rows(entry, key):
|
|
618
|
+
return "alias_missing"
|
|
619
|
+
rows = _expected_aliases(entry, operation)
|
|
620
|
+
if not rows:
|
|
621
|
+
return "expect_mismatch"
|
|
622
|
+
if len(rows) > 1:
|
|
623
|
+
return "duplicate_alias"
|
|
624
|
+
row = rows[0]
|
|
625
|
+
if row.get("effective_to") is not None:
|
|
626
|
+
return "alias_not_open"
|
|
627
|
+
starts = _instant(row.get("effective_from"))
|
|
628
|
+
if starts is not None and _instant(operation.effective_to) <= starts:
|
|
629
|
+
return "not_after_start"
|
|
630
|
+
# A dated alias must cite its source, so a close supplies one for an
|
|
631
|
+
# alias that has none, and never replaces one it has.
|
|
632
|
+
if _text(row.get("source_url")) is None:
|
|
633
|
+
if operation.source_url is None:
|
|
634
|
+
return "source_url_needed"
|
|
635
|
+
elif operation.source_url is not None:
|
|
636
|
+
return "source_url_exists"
|
|
512
637
|
return None
|
|
513
638
|
model_index, rows, reason = _locate(document, operation)
|
|
514
639
|
if reason is not None:
|
|
@@ -565,10 +690,15 @@ def apply_catalog_delta(
|
|
|
565
690
|
delta = parse_catalog_delta(delta_document)
|
|
566
691
|
if not isinstance(document, Mapping):
|
|
567
692
|
raise CatalogDeltaError("catalog document must be a mapping")
|
|
693
|
+
closing = {
|
|
694
|
+
operation.key: operation.effective_to
|
|
695
|
+
for operation in delta.operations
|
|
696
|
+
if operation.op == "close_alias"
|
|
697
|
+
}
|
|
568
698
|
conflicts = tuple(
|
|
569
699
|
DeltaConflict(operation.index, operation.op, operation.key, reason)
|
|
570
700
|
for operation in delta.operations
|
|
571
|
-
if (reason := _precondition(document, operation)) is not None
|
|
701
|
+
if (reason := _precondition(document, operation, closing)) is not None
|
|
572
702
|
)
|
|
573
703
|
if conflicts:
|
|
574
704
|
raise CatalogDeltaConflict(conflicts)
|
|
@@ -586,6 +716,16 @@ def apply_catalog_delta(
|
|
|
586
716
|
copy.deepcopy(dict(operation.alias))
|
|
587
717
|
)
|
|
588
718
|
continue
|
|
719
|
+
if operation.op == "close_alias":
|
|
720
|
+
[model_index] = _model_indexes(result, operation.key.canonical_id)
|
|
721
|
+
# The first equal row is the one checked: an alias this delta
|
|
722
|
+
# adds under the same key starts after this one ends, so it never
|
|
723
|
+
# equals ``expect``.
|
|
724
|
+
alias = _expected_aliases(result["models"][model_index], operation)[0]
|
|
725
|
+
alias["effective_to"] = operation.effective_to
|
|
726
|
+
if operation.source_url is not None:
|
|
727
|
+
alias["source_url"] = operation.source_url
|
|
728
|
+
continue
|
|
589
729
|
model_index, rows, _ = _locate(result, operation)
|
|
590
730
|
prices = result["models"][model_index].setdefault("prices", [])
|
|
591
731
|
if operation.op == "add_price":
|
|
@@ -4985,7 +4985,7 @@ models:
|
|
|
4985
4985
|
cache_read_per_mtok: 0.125
|
|
4986
4986
|
batch_input_per_mtok: 0.625
|
|
4987
4987
|
batch_output_per_mtok: 5
|
|
4988
|
-
source_url: https://
|
|
4988
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5
|
|
4989
4989
|
- channel: vercel-ai-gateway
|
|
4990
4990
|
region: global
|
|
4991
4991
|
effective_from: "2026-10-02"
|
|
@@ -5010,7 +5010,7 @@ models:
|
|
|
5010
5010
|
input_per_mtok: 1.75
|
|
5011
5011
|
output_per_mtok: 14
|
|
5012
5012
|
cache_read_per_mtok: 0.175
|
|
5013
|
-
source_url: https://
|
|
5013
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.3-codex
|
|
5014
5014
|
- channel: vercel-ai-gateway
|
|
5015
5015
|
region: global
|
|
5016
5016
|
effective_from: "2026-10-02"
|
|
@@ -5141,11 +5141,22 @@ models:
|
|
|
5141
5141
|
- channel: openai-api
|
|
5142
5142
|
region: global
|
|
5143
5143
|
effective_from: "2026-07-09"
|
|
5144
|
+
effective_to: "2026-10-27"
|
|
5144
5145
|
input_per_mtok: 12.5
|
|
5145
5146
|
output_per_mtok: 75
|
|
5146
5147
|
cache_read_per_mtok: 1.25
|
|
5147
5148
|
cache_write_5m_per_mtok: 15.625
|
|
5148
|
-
source_url: https://
|
|
5149
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.6-cyber
|
|
5150
|
+
- channel: openai-api
|
|
5151
|
+
region: global
|
|
5152
|
+
effective_from: "2026-10-27"
|
|
5153
|
+
input_per_mtok: 12.5
|
|
5154
|
+
output_per_mtok: 75
|
|
5155
|
+
cache_read_per_mtok: 1.25
|
|
5156
|
+
cache_write_5m_per_mtok: 15.625
|
|
5157
|
+
rules:
|
|
5158
|
+
long_context: {threshold: 272000, threshold_counts_cached_tokens: true, input_multiplier: 2, output_multiplier: 1.5}
|
|
5159
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.6-cyber
|
|
5149
5160
|
- canonical_id: openai/o4-mini
|
|
5150
5161
|
publisher: openai
|
|
5151
5162
|
aliases:
|
|
@@ -5163,7 +5174,7 @@ models:
|
|
|
5163
5174
|
cache_read_per_mtok: 0.275
|
|
5164
5175
|
batch_input_per_mtok: 0.55
|
|
5165
5176
|
batch_output_per_mtok: 2.2
|
|
5166
|
-
source_url: https://
|
|
5177
|
+
source_url: https://developers.openai.com/api/docs/models/o4-mini
|
|
5167
5178
|
- channel: vercel-ai-gateway
|
|
5168
5179
|
region: global
|
|
5169
5180
|
effective_from: "2025-04-16"
|
|
@@ -619,6 +619,8 @@ def _gateway_price(
|
|
|
619
619
|
|
|
620
620
|
# Reviewed gateway spellings for publishers the catalog already names.
|
|
621
621
|
_GATEWAY_PUBLISHER_PREFIXES = {
|
|
622
|
+
"bytedance-seed": "bytedance",
|
|
623
|
+
"meta-llama": "meta",
|
|
622
624
|
"mistralai": "mistral",
|
|
623
625
|
"qwen": "alibaba",
|
|
624
626
|
"z-ai": "zai",
|
|
@@ -642,8 +644,48 @@ def _identity_shape(model_key: str) -> tuple[str, str]:
|
|
|
642
644
|
return prefix, "".join(character for character in suffix if character.isalnum())
|
|
643
645
|
|
|
644
646
|
|
|
647
|
+
def _current_identity(
|
|
648
|
+
entry: Mapping[str, Any], day: date
|
|
649
|
+
) -> tuple[str | None, list[Mapping[str, Any]]]:
|
|
650
|
+
"""``entry``'s canonical id and its aliases in effect on ``day``.
|
|
651
|
+
|
|
652
|
+
An alias that has ended no longer names the model, so a spelling moved to
|
|
653
|
+
another model matches only there. The canonical id is left out when it is
|
|
654
|
+
also one of the model's alias spellings and none of those is in effect,
|
|
655
|
+
as ``_names`` treats the canonical tail."""
|
|
656
|
+
aliases = [a for a in entry.get("aliases") or [] if isinstance(a, Mapping)]
|
|
657
|
+
current = [a for a in aliases if _in_effect(a, day, dated=False)]
|
|
658
|
+
canonical_id = str(entry.get("canonical_id") or "").strip().lower()
|
|
659
|
+
spelled = {str(a.get("alias") or "").strip().lower() for a in aliases}
|
|
660
|
+
active = {str(a.get("alias") or "").strip().lower() for a in current}
|
|
661
|
+
if canonical_id in spelled and canonical_id not in active:
|
|
662
|
+
return None, current
|
|
663
|
+
return canonical_id, current
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _channel_models(
|
|
667
|
+
document: Mapping[str, Any], source: PriceSource, model_key: str, day: date
|
|
668
|
+
) -> list[Mapping[str, Any]]:
|
|
669
|
+
"""Models with an alias in effect on ``day`` that spells ``model_key`` on
|
|
670
|
+
the source's own channel. That alias is the reviewed mapping for this
|
|
671
|
+
gateway, so it decides even while another channel still gives the
|
|
672
|
+
spelling to a different model."""
|
|
673
|
+
provider, wanted = source.provider.lower(), model_key.strip().lower()
|
|
674
|
+
return [
|
|
675
|
+
entry
|
|
676
|
+
for entry in document.get("models") or []
|
|
677
|
+
if isinstance(entry, Mapping)
|
|
678
|
+
and any(
|
|
679
|
+
str(alias.get("provider") or "").strip().lower() == provider
|
|
680
|
+
and str(alias.get("alias") or "").strip().lower() == wanted
|
|
681
|
+
and alias.get("channel") == source.channel
|
|
682
|
+
for alias in _current_identity(entry, day)[1]
|
|
683
|
+
)
|
|
684
|
+
]
|
|
685
|
+
|
|
686
|
+
|
|
645
687
|
def _exact_models(
|
|
646
|
-
document: Mapping[str, Any], model_key: str
|
|
688
|
+
document: Mapping[str, Any], model_key: str, day: date
|
|
647
689
|
) -> list[Mapping[str, Any]]:
|
|
648
690
|
publisher, _, _ = _catalog_gateway_identity(model_key).partition("/")
|
|
649
691
|
wanted = {
|
|
@@ -654,17 +696,15 @@ def _exact_models(
|
|
|
654
696
|
for entry in document.get("models") or []:
|
|
655
697
|
if not isinstance(entry, Mapping):
|
|
656
698
|
continue
|
|
657
|
-
|
|
699
|
+
canonical_id, aliases = _current_identity(entry, day)
|
|
700
|
+
names = {canonical_id} if canonical_id else set()
|
|
658
701
|
names.update(
|
|
659
|
-
str(alias.get("alias") or "").strip().lower()
|
|
660
|
-
for alias in entry.get("aliases") or []
|
|
661
|
-
if isinstance(alias, Mapping)
|
|
702
|
+
str(alias.get("alias") or "").strip().lower() for alias in aliases
|
|
662
703
|
)
|
|
663
704
|
names.update(
|
|
664
705
|
f"{publisher}/{str(alias.get('alias') or '').strip().lower()}"
|
|
665
|
-
for alias in
|
|
666
|
-
if
|
|
667
|
-
and _catalog_publisher(alias.get("provider")) == publisher
|
|
706
|
+
for alias in aliases
|
|
707
|
+
if _catalog_publisher(alias.get("provider")) == publisher
|
|
668
708
|
)
|
|
669
709
|
if wanted & names:
|
|
670
710
|
found.append(entry)
|
|
@@ -672,21 +712,21 @@ def _exact_models(
|
|
|
672
712
|
|
|
673
713
|
|
|
674
714
|
def _near_models(
|
|
675
|
-
document: Mapping[str, Any], model_key: str
|
|
715
|
+
document: Mapping[str, Any], model_key: str, day: date
|
|
676
716
|
) -> list[Mapping[str, Any]]:
|
|
677
717
|
wanted = _identity_shape(model_key)
|
|
678
718
|
found = []
|
|
679
719
|
for entry in document.get("models") or []:
|
|
680
720
|
if not isinstance(entry, Mapping):
|
|
681
721
|
continue
|
|
682
|
-
|
|
722
|
+
canonical_id, aliases = _current_identity(entry, day)
|
|
723
|
+
shapes = {_identity_shape(canonical_id)} if canonical_id else set()
|
|
683
724
|
shapes.update(
|
|
684
725
|
_identity_shape(
|
|
685
726
|
f"{_catalog_publisher(alias.get('provider'))}/"
|
|
686
727
|
f"{str(alias.get('alias') or '').strip().lower()}"
|
|
687
728
|
)
|
|
688
|
-
for alias in
|
|
689
|
-
if isinstance(alias, Mapping)
|
|
729
|
+
for alias in aliases
|
|
690
730
|
)
|
|
691
731
|
if wanted in shapes:
|
|
692
732
|
found.append(entry)
|
|
@@ -703,9 +743,11 @@ def _gateway_additions(
|
|
|
703
743
|
operations: list[dict[str, Any]] = []
|
|
704
744
|
identity_exclusions: list[dict[str, Any]] = []
|
|
705
745
|
for rate in extraction.rates:
|
|
706
|
-
matches =
|
|
746
|
+
matches = _channel_models(
|
|
747
|
+
document, source, rate.model_key, observed_on
|
|
748
|
+
) or _exact_models(document, rate.model_key, observed_on)
|
|
707
749
|
if not matches:
|
|
708
|
-
near = _near_models(document, rate.model_key)
|
|
750
|
+
near = _near_models(document, rate.model_key, observed_on)
|
|
709
751
|
if near:
|
|
710
752
|
identity_exclusions.append(
|
|
711
753
|
{
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metergraph-core
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
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
|
|
@@ -356,6 +356,41 @@ effective_from)` on a model the catalog already has:
|
|
|
356
356
|
start on a later UTC date than the row it replaces, for the same reason.
|
|
357
357
|
- `close_price` sets `effective_to` on the open row without a replacement.
|
|
358
358
|
|
|
359
|
+
Catalog operations change which models exist and what calls they match:
|
|
360
|
+
|
|
361
|
+
- `add_model` appends one complete model entry.
|
|
362
|
+
- `add_alias` appends one alias to an existing model. A spelling another
|
|
363
|
+
model already holds is a conflict only while both aliases are in effect.
|
|
364
|
+
- `close_alias` sets `effective_to` on an open alias, named by
|
|
365
|
+
`(canonical_id, provider, alias, channel)`; `expect` is the exact current
|
|
366
|
+
alias. An alias with dates must cite its source, so a close carries
|
|
367
|
+
`source_url` when the alias has none and must not when it has one.
|
|
368
|
+
|
|
369
|
+
A `close_alias`, `close_price`, `add_alias` and `add_price` together move a
|
|
370
|
+
spelling from a duplicate entry to the model it duplicates from a date. Calls
|
|
371
|
+
before that date keep the old model and price, calls from it resolve to the
|
|
372
|
+
surviving model, and the old entry stays for history with nothing in effect:
|
|
373
|
+
|
|
374
|
+
```yaml
|
|
375
|
+
- op: close_alias
|
|
376
|
+
key: {canonical_id: example-host/model-a-instruct, provider: openrouter,
|
|
377
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
378
|
+
expect: {provider: openrouter, alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
379
|
+
effective_to: "2026-10-20"
|
|
380
|
+
source_url: "https://..."
|
|
381
|
+
- op: close_price # the duplicate's open openrouter-api row
|
|
382
|
+
- op: add_alias
|
|
383
|
+
key: {canonical_id: example/model-a, provider: openrouter,
|
|
384
|
+
alias: example-host/model-a-instruct, channel: openrouter-api}
|
|
385
|
+
expect: null
|
|
386
|
+
alias: {provider: openrouter, alias: example-host/model-a-instruct,
|
|
387
|
+
channel: openrouter-api, effective_from: "2026-10-20", source_url: "https://..."}
|
|
388
|
+
- op: add_price # the same rate on example/model-a from 2026-10-20
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Gateway discovery matches a spelling only through aliases in effect on the
|
|
392
|
+
observation date, so it follows a moved spelling to its new model.
|
|
393
|
+
|
|
359
394
|
A price id ends with the UTC date of its row's start. If two rows for one
|
|
360
395
|
model, channel and region would still share an id (possible in a hand-edited
|
|
361
396
|
catalog), each that does not start at UTC midnight also names its UTC start
|
|
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
|
|
File without changes
|
|
File without changes
|
{metergraph_core-0.8.1 → metergraph_core-0.8.2}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|