metergraph-core 0.8.1__tar.gz → 0.8.3__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.3}/PKG-INFO +36 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/README.md +35 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/pyproject.toml +1 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/catalog_delta.py +157 -17
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/prices.yaml +171 -5
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/refresh.py +56 -14
- {metergraph_core-0.8.1 → metergraph_core-0.8.3/src/metergraph_core.egg-info}/PKG-INFO +36 -1
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/MANIFEST.in +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/setup.cfg +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/__init__.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/billing.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/catalog.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/models.yaml +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/sources.yaml +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/extractors.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/loader.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/models.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/responses.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/retrieval.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/sources.py +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/requires.txt +0 -0
- {metergraph_core-0.8.1 → metergraph_core-0.8.3}/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.3
|
|
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.3"
|
|
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":
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Community updates welcome: add an alias or price entry with its provider
|
|
8
8
|
# source_url and open a PR. CI validates structure and date overlaps.
|
|
9
|
-
version: "2026-10-
|
|
9
|
+
version: "2026-10-28"
|
|
10
10
|
currency: USD
|
|
11
11
|
pricing_verified_at: "2026-10-27"
|
|
12
12
|
models:
|
|
@@ -2293,6 +2293,14 @@ models:
|
|
|
2293
2293
|
publisher: deepseek
|
|
2294
2294
|
aliases:
|
|
2295
2295
|
- {provider: bedrock, alias: deepseek.v3-v1:0, channel: aws-bedrock}
|
|
2296
|
+
- provider: vercel
|
|
2297
|
+
alias: deepseek/deepseek-v3.1
|
|
2298
|
+
channel: vercel-ai-gateway
|
|
2299
|
+
rules:
|
|
2300
|
+
input_includes_cache_read: true
|
|
2301
|
+
input_includes_cache_write: true
|
|
2302
|
+
effective_from: "2026-11-02"
|
|
2303
|
+
source_url: https://vercel.com/ai-gateway/models/deepseek-v3.1
|
|
2296
2304
|
prices:
|
|
2297
2305
|
- channel: aws-bedrock
|
|
2298
2306
|
region: us-west-2
|
|
@@ -2300,6 +2308,15 @@ models:
|
|
|
2300
2308
|
input_per_mtok: 0.58
|
|
2301
2309
|
output_per_mtok: 1.68
|
|
2302
2310
|
source_url: https://aws.amazon.com/bedrock/pricing/
|
|
2311
|
+
- channel: vercel-ai-gateway
|
|
2312
|
+
region: global
|
|
2313
|
+
effective_from: "2026-11-02"
|
|
2314
|
+
input_per_mtok: 0.25
|
|
2315
|
+
output_per_mtok: 0.95
|
|
2316
|
+
cache_read_per_mtok: 0.13
|
|
2317
|
+
rules:
|
|
2318
|
+
varies_by_provider: true
|
|
2319
|
+
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
2303
2320
|
|
|
2304
2321
|
- canonical_id: deepseek/v4-flash
|
|
2305
2322
|
publisher: deepseek
|
|
@@ -4985,7 +5002,7 @@ models:
|
|
|
4985
5002
|
cache_read_per_mtok: 0.125
|
|
4986
5003
|
batch_input_per_mtok: 0.625
|
|
4987
5004
|
batch_output_per_mtok: 5
|
|
4988
|
-
source_url: https://
|
|
5005
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5
|
|
4989
5006
|
- channel: vercel-ai-gateway
|
|
4990
5007
|
region: global
|
|
4991
5008
|
effective_from: "2026-10-02"
|
|
@@ -5010,7 +5027,7 @@ models:
|
|
|
5010
5027
|
input_per_mtok: 1.75
|
|
5011
5028
|
output_per_mtok: 14
|
|
5012
5029
|
cache_read_per_mtok: 0.175
|
|
5013
|
-
source_url: https://
|
|
5030
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.3-codex
|
|
5014
5031
|
- channel: vercel-ai-gateway
|
|
5015
5032
|
region: global
|
|
5016
5033
|
effective_from: "2026-10-02"
|
|
@@ -5141,11 +5158,22 @@ models:
|
|
|
5141
5158
|
- channel: openai-api
|
|
5142
5159
|
region: global
|
|
5143
5160
|
effective_from: "2026-07-09"
|
|
5161
|
+
effective_to: "2026-10-27"
|
|
5144
5162
|
input_per_mtok: 12.5
|
|
5145
5163
|
output_per_mtok: 75
|
|
5146
5164
|
cache_read_per_mtok: 1.25
|
|
5147
5165
|
cache_write_5m_per_mtok: 15.625
|
|
5148
|
-
source_url: https://
|
|
5166
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.6-cyber
|
|
5167
|
+
- channel: openai-api
|
|
5168
|
+
region: global
|
|
5169
|
+
effective_from: "2026-10-27"
|
|
5170
|
+
input_per_mtok: 12.5
|
|
5171
|
+
output_per_mtok: 75
|
|
5172
|
+
cache_read_per_mtok: 1.25
|
|
5173
|
+
cache_write_5m_per_mtok: 15.625
|
|
5174
|
+
rules:
|
|
5175
|
+
long_context: {threshold: 272000, threshold_counts_cached_tokens: true, input_multiplier: 2, output_multiplier: 1.5}
|
|
5176
|
+
source_url: https://developers.openai.com/api/docs/models/gpt-5.6-cyber
|
|
5149
5177
|
- canonical_id: openai/o4-mini
|
|
5150
5178
|
publisher: openai
|
|
5151
5179
|
aliases:
|
|
@@ -5163,7 +5191,7 @@ models:
|
|
|
5163
5191
|
cache_read_per_mtok: 0.275
|
|
5164
5192
|
batch_input_per_mtok: 0.55
|
|
5165
5193
|
batch_output_per_mtok: 2.2
|
|
5166
|
-
source_url: https://
|
|
5194
|
+
source_url: https://developers.openai.com/api/docs/models/o4-mini
|
|
5167
5195
|
- channel: vercel-ai-gateway
|
|
5168
5196
|
region: global
|
|
5169
5197
|
effective_from: "2025-04-16"
|
|
@@ -5705,6 +5733,14 @@ models:
|
|
|
5705
5733
|
rules:
|
|
5706
5734
|
input_includes_cache_read: true
|
|
5707
5735
|
input_includes_cache_write: true
|
|
5736
|
+
- provider: openrouter
|
|
5737
|
+
alias: qwen/qwen3-coder-30b-a3b-instruct
|
|
5738
|
+
channel: openrouter-api
|
|
5739
|
+
rules:
|
|
5740
|
+
input_includes_cache_read: true
|
|
5741
|
+
input_includes_cache_write: true
|
|
5742
|
+
effective_from: "2026-11-02"
|
|
5743
|
+
source_url: https://openrouter.ai/qwen/qwen3-coder-30b-a3b-instruct
|
|
5708
5744
|
prices:
|
|
5709
5745
|
- channel: vercel-ai-gateway
|
|
5710
5746
|
region: global
|
|
@@ -5712,6 +5748,14 @@ models:
|
|
|
5712
5748
|
input_per_mtok: 0.15
|
|
5713
5749
|
output_per_mtok: 0.6
|
|
5714
5750
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
5751
|
+
- channel: openrouter-api
|
|
5752
|
+
region: global
|
|
5753
|
+
effective_from: "2026-11-02"
|
|
5754
|
+
input_per_mtok: 0.07
|
|
5755
|
+
output_per_mtok: 0.28
|
|
5756
|
+
rules:
|
|
5757
|
+
varies_by_provider: true
|
|
5758
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
5715
5759
|
|
|
5716
5760
|
- canonical_id: alibaba/qwen3-coder-next
|
|
5717
5761
|
publisher: alibaba
|
|
@@ -6286,6 +6330,14 @@ models:
|
|
|
6286
6330
|
rules:
|
|
6287
6331
|
input_includes_cache_read: true
|
|
6288
6332
|
input_includes_cache_write: true
|
|
6333
|
+
- provider: openrouter
|
|
6334
|
+
alias: bytedance-seed/seed-2-1-turbo
|
|
6335
|
+
channel: openrouter-api
|
|
6336
|
+
rules:
|
|
6337
|
+
input_includes_cache_read: true
|
|
6338
|
+
input_includes_cache_write: true
|
|
6339
|
+
effective_from: "2026-11-02"
|
|
6340
|
+
source_url: https://openrouter.ai/bytedance-seed/seed-2-1-turbo
|
|
6289
6341
|
prices:
|
|
6290
6342
|
- channel: vercel-ai-gateway
|
|
6291
6343
|
region: global
|
|
@@ -6294,6 +6346,14 @@ models:
|
|
|
6294
6346
|
output_per_mtok: 2.5
|
|
6295
6347
|
cache_read_per_mtok: 0.1
|
|
6296
6348
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
6349
|
+
- channel: openrouter-api
|
|
6350
|
+
region: global
|
|
6351
|
+
effective_from: "2026-11-02"
|
|
6352
|
+
input_per_mtok: 0.5
|
|
6353
|
+
output_per_mtok: 2.5
|
|
6354
|
+
rules:
|
|
6355
|
+
varies_by_provider: true
|
|
6356
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6297
6357
|
|
|
6298
6358
|
- canonical_id: callstack/apex
|
|
6299
6359
|
publisher: callstack
|
|
@@ -6372,10 +6432,13 @@ models:
|
|
|
6372
6432
|
rules:
|
|
6373
6433
|
input_includes_cache_read: true
|
|
6374
6434
|
input_includes_cache_write: true
|
|
6435
|
+
effective_to: "2026-11-02"
|
|
6436
|
+
source_url: https://vercel.com/ai-gateway/models/deepseek-v3.1
|
|
6375
6437
|
prices:
|
|
6376
6438
|
- channel: vercel-ai-gateway
|
|
6377
6439
|
region: global
|
|
6378
6440
|
effective_from: "2026-10-20"
|
|
6441
|
+
effective_to: "2026-11-02"
|
|
6379
6442
|
input_per_mtok: 0.25
|
|
6380
6443
|
output_per_mtok: 0.95
|
|
6381
6444
|
cache_read_per_mtok: 0.13
|
|
@@ -6825,6 +6888,14 @@ models:
|
|
|
6825
6888
|
rules:
|
|
6826
6889
|
input_includes_cache_read: true
|
|
6827
6890
|
input_includes_cache_write: true
|
|
6891
|
+
- provider: openrouter
|
|
6892
|
+
alias: meta-llama/llama-3.1-70b-instruct
|
|
6893
|
+
channel: openrouter-api
|
|
6894
|
+
rules:
|
|
6895
|
+
input_includes_cache_read: true
|
|
6896
|
+
input_includes_cache_write: true
|
|
6897
|
+
effective_from: "2026-11-02"
|
|
6898
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.1-70b-instruct
|
|
6828
6899
|
prices:
|
|
6829
6900
|
- channel: vercel-ai-gateway
|
|
6830
6901
|
region: global
|
|
@@ -6832,6 +6903,14 @@ models:
|
|
|
6832
6903
|
input_per_mtok: 0.72
|
|
6833
6904
|
output_per_mtok: 0.72
|
|
6834
6905
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
6906
|
+
- channel: openrouter-api
|
|
6907
|
+
region: global
|
|
6908
|
+
effective_from: "2026-11-02"
|
|
6909
|
+
input_per_mtok: 0.4
|
|
6910
|
+
output_per_mtok: 0.4
|
|
6911
|
+
rules:
|
|
6912
|
+
varies_by_provider: true
|
|
6913
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6835
6914
|
|
|
6836
6915
|
- canonical_id: meta/llama-3.1-8b
|
|
6837
6916
|
publisher: meta
|
|
@@ -6842,6 +6921,14 @@ models:
|
|
|
6842
6921
|
rules:
|
|
6843
6922
|
input_includes_cache_read: true
|
|
6844
6923
|
input_includes_cache_write: true
|
|
6924
|
+
- provider: openrouter
|
|
6925
|
+
alias: meta-llama/llama-3.1-8b-instruct
|
|
6926
|
+
channel: openrouter-api
|
|
6927
|
+
rules:
|
|
6928
|
+
input_includes_cache_read: true
|
|
6929
|
+
input_includes_cache_write: true
|
|
6930
|
+
effective_from: "2026-11-02"
|
|
6931
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.1-8b-instruct
|
|
6845
6932
|
prices:
|
|
6846
6933
|
- channel: vercel-ai-gateway
|
|
6847
6934
|
region: global
|
|
@@ -6851,6 +6938,15 @@ models:
|
|
|
6851
6938
|
rules:
|
|
6852
6939
|
varies_by_provider: true
|
|
6853
6940
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
6941
|
+
- channel: openrouter-api
|
|
6942
|
+
region: global
|
|
6943
|
+
effective_from: "2026-11-02"
|
|
6944
|
+
input_per_mtok: 0.05
|
|
6945
|
+
output_per_mtok: 0.08
|
|
6946
|
+
cache_read_per_mtok: 0.025
|
|
6947
|
+
rules:
|
|
6948
|
+
varies_by_provider: true
|
|
6949
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6854
6950
|
|
|
6855
6951
|
- canonical_id: meta/llama-3.3-70b
|
|
6856
6952
|
publisher: meta
|
|
@@ -6861,6 +6957,14 @@ models:
|
|
|
6861
6957
|
rules:
|
|
6862
6958
|
input_includes_cache_read: true
|
|
6863
6959
|
input_includes_cache_write: true
|
|
6960
|
+
- provider: openrouter
|
|
6961
|
+
alias: meta-llama/llama-3.3-70b-instruct
|
|
6962
|
+
channel: openrouter-api
|
|
6963
|
+
rules:
|
|
6964
|
+
input_includes_cache_read: true
|
|
6965
|
+
input_includes_cache_write: true
|
|
6966
|
+
effective_from: "2026-11-02"
|
|
6967
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.3-70b-instruct
|
|
6864
6968
|
prices:
|
|
6865
6969
|
- channel: vercel-ai-gateway
|
|
6866
6970
|
region: global
|
|
@@ -6868,6 +6972,14 @@ models:
|
|
|
6868
6972
|
input_per_mtok: 0.72
|
|
6869
6973
|
output_per_mtok: 0.72
|
|
6870
6974
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
6975
|
+
- channel: openrouter-api
|
|
6976
|
+
region: global
|
|
6977
|
+
effective_from: "2026-11-02"
|
|
6978
|
+
input_per_mtok: 0.1
|
|
6979
|
+
output_per_mtok: 0.32
|
|
6980
|
+
rules:
|
|
6981
|
+
varies_by_provider: true
|
|
6982
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6871
6983
|
|
|
6872
6984
|
- canonical_id: meta/llama-4-maverick
|
|
6873
6985
|
publisher: meta
|
|
@@ -6878,6 +6990,14 @@ models:
|
|
|
6878
6990
|
rules:
|
|
6879
6991
|
input_includes_cache_read: true
|
|
6880
6992
|
input_includes_cache_write: true
|
|
6993
|
+
- provider: openrouter
|
|
6994
|
+
alias: meta-llama/llama-4-maverick
|
|
6995
|
+
channel: openrouter-api
|
|
6996
|
+
rules:
|
|
6997
|
+
input_includes_cache_read: true
|
|
6998
|
+
input_includes_cache_write: true
|
|
6999
|
+
effective_from: "2026-11-02"
|
|
7000
|
+
source_url: https://openrouter.ai/meta-llama/llama-4-maverick
|
|
6881
7001
|
prices:
|
|
6882
7002
|
- channel: vercel-ai-gateway
|
|
6883
7003
|
region: global
|
|
@@ -6885,6 +7005,15 @@ models:
|
|
|
6885
7005
|
input_per_mtok: 0.24
|
|
6886
7006
|
output_per_mtok: 0.97
|
|
6887
7007
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
7008
|
+
- channel: openrouter-api
|
|
7009
|
+
region: global
|
|
7010
|
+
effective_from: "2026-11-02"
|
|
7011
|
+
input_per_mtok: 0.1875
|
|
7012
|
+
output_per_mtok: 0.6525
|
|
7013
|
+
cache_read_per_mtok: 0.05
|
|
7014
|
+
rules:
|
|
7015
|
+
varies_by_provider: true
|
|
7016
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6888
7017
|
|
|
6889
7018
|
- canonical_id: meta/llama-4-scout
|
|
6890
7019
|
publisher: meta
|
|
@@ -6895,6 +7024,14 @@ models:
|
|
|
6895
7024
|
rules:
|
|
6896
7025
|
input_includes_cache_read: true
|
|
6897
7026
|
input_includes_cache_write: true
|
|
7027
|
+
- provider: openrouter
|
|
7028
|
+
alias: meta-llama/llama-4-scout
|
|
7029
|
+
channel: openrouter-api
|
|
7030
|
+
rules:
|
|
7031
|
+
input_includes_cache_read: true
|
|
7032
|
+
input_includes_cache_write: true
|
|
7033
|
+
effective_from: "2026-11-02"
|
|
7034
|
+
source_url: https://openrouter.ai/meta-llama/llama-4-scout
|
|
6898
7035
|
prices:
|
|
6899
7036
|
- channel: vercel-ai-gateway
|
|
6900
7037
|
region: global
|
|
@@ -6904,6 +7041,14 @@ models:
|
|
|
6904
7041
|
rules:
|
|
6905
7042
|
varies_by_provider: true
|
|
6906
7043
|
source_url: https://ai-gateway.vercel.sh/v1/models
|
|
7044
|
+
- channel: openrouter-api
|
|
7045
|
+
region: global
|
|
7046
|
+
effective_from: "2026-11-02"
|
|
7047
|
+
input_per_mtok: 0.1
|
|
7048
|
+
output_per_mtok: 0.3
|
|
7049
|
+
rules:
|
|
7050
|
+
varies_by_provider: true
|
|
7051
|
+
source_url: https://openrouter.ai/api/v1/models
|
|
6907
7052
|
|
|
6908
7053
|
- canonical_id: meta/muse-spark-1.2-contributor
|
|
6909
7054
|
publisher: meta
|
|
@@ -8854,10 +8999,13 @@ models:
|
|
|
8854
8999
|
rules:
|
|
8855
9000
|
input_includes_cache_read: true
|
|
8856
9001
|
input_includes_cache_write: true
|
|
9002
|
+
effective_to: "2026-11-02"
|
|
9003
|
+
source_url: https://openrouter.ai/bytedance-seed/seed-2-1-turbo
|
|
8857
9004
|
prices:
|
|
8858
9005
|
- channel: openrouter-api
|
|
8859
9006
|
region: global
|
|
8860
9007
|
effective_from: "2026-10-20"
|
|
9008
|
+
effective_to: "2026-11-02"
|
|
8861
9009
|
input_per_mtok: 0.5
|
|
8862
9010
|
output_per_mtok: 2.5
|
|
8863
9011
|
rules:
|
|
@@ -9282,10 +9430,13 @@ models:
|
|
|
9282
9430
|
rules:
|
|
9283
9431
|
input_includes_cache_read: true
|
|
9284
9432
|
input_includes_cache_write: true
|
|
9433
|
+
effective_to: "2026-11-02"
|
|
9434
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.1-70b-instruct
|
|
9285
9435
|
prices:
|
|
9286
9436
|
- channel: openrouter-api
|
|
9287
9437
|
region: global
|
|
9288
9438
|
effective_from: "2026-10-20"
|
|
9439
|
+
effective_to: "2026-11-02"
|
|
9289
9440
|
input_per_mtok: 0.4
|
|
9290
9441
|
output_per_mtok: 0.4
|
|
9291
9442
|
rules:
|
|
@@ -9301,10 +9452,13 @@ models:
|
|
|
9301
9452
|
rules:
|
|
9302
9453
|
input_includes_cache_read: true
|
|
9303
9454
|
input_includes_cache_write: true
|
|
9455
|
+
effective_to: "2026-11-02"
|
|
9456
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.1-8b-instruct
|
|
9304
9457
|
prices:
|
|
9305
9458
|
- channel: openrouter-api
|
|
9306
9459
|
region: global
|
|
9307
9460
|
effective_from: "2026-10-20"
|
|
9461
|
+
effective_to: "2026-11-02"
|
|
9308
9462
|
input_per_mtok: 0.05
|
|
9309
9463
|
output_per_mtok: 0.08
|
|
9310
9464
|
cache_read_per_mtok: 0.025
|
|
@@ -9359,10 +9513,13 @@ models:
|
|
|
9359
9513
|
rules:
|
|
9360
9514
|
input_includes_cache_read: true
|
|
9361
9515
|
input_includes_cache_write: true
|
|
9516
|
+
effective_to: "2026-11-02"
|
|
9517
|
+
source_url: https://openrouter.ai/meta-llama/llama-3.3-70b-instruct
|
|
9362
9518
|
prices:
|
|
9363
9519
|
- channel: openrouter-api
|
|
9364
9520
|
region: global
|
|
9365
9521
|
effective_from: "2026-10-20"
|
|
9522
|
+
effective_to: "2026-11-02"
|
|
9366
9523
|
input_per_mtok: 0.1
|
|
9367
9524
|
output_per_mtok: 0.32
|
|
9368
9525
|
rules:
|
|
@@ -9378,10 +9535,13 @@ models:
|
|
|
9378
9535
|
rules:
|
|
9379
9536
|
input_includes_cache_read: true
|
|
9380
9537
|
input_includes_cache_write: true
|
|
9538
|
+
effective_to: "2026-11-02"
|
|
9539
|
+
source_url: https://openrouter.ai/meta-llama/llama-4-maverick
|
|
9381
9540
|
prices:
|
|
9382
9541
|
- channel: openrouter-api
|
|
9383
9542
|
region: global
|
|
9384
9543
|
effective_from: "2026-10-20"
|
|
9544
|
+
effective_to: "2026-11-02"
|
|
9385
9545
|
input_per_mtok: 0.1875
|
|
9386
9546
|
output_per_mtok: 0.6525
|
|
9387
9547
|
cache_read_per_mtok: 0.05
|
|
@@ -9398,10 +9558,13 @@ models:
|
|
|
9398
9558
|
rules:
|
|
9399
9559
|
input_includes_cache_read: true
|
|
9400
9560
|
input_includes_cache_write: true
|
|
9561
|
+
effective_to: "2026-11-02"
|
|
9562
|
+
source_url: https://openrouter.ai/meta-llama/llama-4-scout
|
|
9401
9563
|
prices:
|
|
9402
9564
|
- channel: openrouter-api
|
|
9403
9565
|
region: global
|
|
9404
9566
|
effective_from: "2026-10-20"
|
|
9567
|
+
effective_to: "2026-11-02"
|
|
9405
9568
|
input_per_mtok: 0.1
|
|
9406
9569
|
output_per_mtok: 0.3
|
|
9407
9570
|
rules:
|
|
@@ -10369,10 +10532,13 @@ models:
|
|
|
10369
10532
|
rules:
|
|
10370
10533
|
input_includes_cache_read: true
|
|
10371
10534
|
input_includes_cache_write: true
|
|
10535
|
+
effective_to: "2026-11-02"
|
|
10536
|
+
source_url: https://openrouter.ai/qwen/qwen3-coder-30b-a3b-instruct
|
|
10372
10537
|
prices:
|
|
10373
10538
|
- channel: openrouter-api
|
|
10374
10539
|
region: global
|
|
10375
10540
|
effective_from: "2026-10-20"
|
|
10541
|
+
effective_to: "2026-11-02"
|
|
10376
10542
|
input_per_mtok: 0.07
|
|
10377
10543
|
output_per_mtok: 0.28
|
|
10378
10544
|
rules:
|
|
@@ -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.3
|
|
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.3}/src/metergraph_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|