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.
Files changed (24) hide show
  1. {metergraph_core-0.8.1/src/metergraph_core.egg-info → metergraph_core-0.8.3}/PKG-INFO +36 -1
  2. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/README.md +35 -0
  3. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/pyproject.toml +1 -1
  4. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/catalog_delta.py +157 -17
  5. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/prices.yaml +171 -5
  6. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/refresh.py +56 -14
  7. {metergraph_core-0.8.1 → metergraph_core-0.8.3/src/metergraph_core.egg-info}/PKG-INFO +36 -1
  8. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/MANIFEST.in +0 -0
  9. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/setup.cfg +0 -0
  10. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/__init__.py +0 -0
  11. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/billing.py +0 -0
  12. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/catalog.py +0 -0
  13. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/models.yaml +0 -0
  14. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/data/sources.yaml +0 -0
  15. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/extractors.py +0 -0
  16. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/loader.py +0 -0
  17. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/models.py +0 -0
  18. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/responses.py +0 -0
  19. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/retrieval.py +0 -0
  20. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core/sources.py +0 -0
  21. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/SOURCES.txt +0 -0
  22. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/dependency_links.txt +0 -0
  23. {metergraph_core-0.8.1 → metergraph_core-0.8.3}/src/metergraph_core.egg-info/requires.txt +0 -0
  24. {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.1
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.1"
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}: add_alias key has unknown fields")
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 == "add_alias":
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 _precondition(document: Mapping[str, Any], operation: DeltaOperation) -> str | None:
484
- """Why ``operation`` does not apply to ``document``, or None when it does."""
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
- for canonical_id, provider, name in _aliases(document):
506
- if (provider, name) == (key.provider, key.alias):
507
- return (
508
- "alias_exists"
509
- if canonical_id == key.canonical_id
510
- else "alias_conflict"
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-27"
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://ai-gateway.vercel.sh/v1/models
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://ai-gateway.vercel.sh/v1/models
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://ai-gateway.vercel.sh/v1/models
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://ai-gateway.vercel.sh/v1/models
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
- names = {str(entry.get("canonical_id") or "").strip().lower()}
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 entry.get("aliases") or []
666
- if isinstance(alias, Mapping)
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
- shapes = {_identity_shape(str(entry.get("canonical_id") or ""))}
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 entry.get("aliases") or []
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 = _exact_models(document, rate.model_key)
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.1
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