Skip to main content

metergraph-core

Reusable model registry, price catalog, and deterministic billing engine for MeterGraph.

metergraph-core owns two separate public datasets. models.yaml records canonical model identity, customer-facing names, executable provider routes, and the ordered candidate fields MeterGraph products offer. prices.yaml records effective-dated rates and billing rules. Core parses and validates both datasets, resolves provider aliases and routes, calculates deterministic costs, and selects a qualified gateway-reported charge or catalog estimate as the effective call cost. A third dataset, sources.yaml, registers the official pricing sources that keep prices.yaml current; see Source registry.

It does not own HTTP routes, database access, migrations, authentication, tenancy, ingest, dashboard code, or any hosted-only concern, and it never reads server environment variables.

Install

python -m pip install metergraph-core

Usage

from datetime import datetime, timezone

from metergraph_core import load_catalog

catalog = load_catalog(region="global")
result = catalog.snapshot.cost(
    provider="openai",
    model="gpt-5.4-mini",
    at=datetime(2026, 8, 17, tzinfo=timezone.utc),
    input_tokens=1000,
    output_tokens=200,
)
print(result.canonical_model, result.price_id, result.cost_usd, result.status)

load_catalog() loads the catalog bundled in the installed package. Pass an explicit path for tests or a self-hosted catalog replacement. The returned LoadedCatalog exposes the declared catalog version, the SHA-256 content_hash of the loaded bytes, the parsed document, and the immutable snapshot used for pricing.

A catalog that is already parsed, for example a canonical JSON document read from storage, loads with load_catalog_document():

from metergraph_core import load_catalog_document

catalog = load_catalog_document(document, region="global")

It validates exactly as load_catalog() does, because both build through the same path, and a file and a document with the same content produce the same catalog. The document is deep-copied, so later changes to the caller's mapping never reach the loaded catalog. Its content_hash is the SHA-256 of the document's canonical JSON (sorted keys, compact separators, ISO dates), so two documents with the same content hash the same whatever their key order. That differs from the raw-byte hash load_catalog() reports for a file.

Planning and evaluation systems that know the deployment channel before making a call can resolve the exact effective price without emulating a provider response:

price = catalog.snapshot.resolve_price(
    model="openai/gpt-5.6-luna",
    channel="vercel-ai-gateway",
    at=datetime(2026, 8, 17, tzinfo=timezone.utc),
)
if price is not None:
    print(
        price.canonical_model,
        price.price.id,
        price.price.input_per_mtok,
        price.price.source_url,
    )

Resolution accepts canonical IDs and channel-scoped aliases, normalizes case and surrounding whitespace, applies the configured region fallback and effective-date windows, and returns None when no exact model/channel price exists. It never substitutes a direct-provider price for a gateway price.

On Bedrock only (provider bedrock for cost, channel aws-bedrock or aws-bedrock-geo for resolve_price), an id with no exact alias falls back to the id without its model-version suffix (anthropic.claude-sonnet-5-v1:0 to anthropic.claude-sonnet-5) and then without a global. inference-profile prefix, the latter only onto an aws-bedrock alias. Geographic prefixes such as us. or eu. are never stripped, because they bill on aws-bedrock-geo.

LoadedCatalog.currency is currently always USD, and LoadedCatalog.pricing_verified_at records when the bundled catalog was last checked against its linked provider sources.

Direct lookup and usage cost

lookup_price returns the price a call is billed at, and calculate_cost costs a call's usage at that price:

from metergraph_core import ModelUsage

found = catalog.lookup_price(
    model="gpt-5.4-mini",
    provider="openai",
    at=datetime(2026, 8, 17, tzinfo=timezone.utc),
)
print(found.status, found.model_id, found.price.id, found.price.currency)

result = catalog.calculate_cost(
    model="gpt-5.4-mini",
    provider="openai",
    at=datetime(2026, 8, 17, tzinfo=timezone.utc),
    usage=ModelUsage(input_tokens=1000, output_tokens=200, cache_read_tokens=500),
)
print(result.status, result.model_id, result.cost, result.currency)
print(result.components.input, result.components.cache_read)

Both resolve exactly as resolve_price and price do, including effective-date windows and the Bedrock fallback, and cost through the same arithmetic. model_id is the resolved catalog ID, not the alias passed in, and price is the applied catalog row. An explicit channel is authoritative; without one, provider supplies its direct channel, and with neither the result is unpriced with missing_dimensions=("channel",).

Every Price states its currency, and CostResult.currency is the currency of the applied price. The bundled catalog is USD. cost and components.input, output, cache_read, cache_write and request are in that currency; the older cost_usd and *_usd names return the same values.

A ModelUsage count left None is unknown, not zero. A missing input or output count keeps the subtotal that is known, marks the result partial, and lists input_tokens or output_tokens in missing_dimensions.

region selects a regional price and overrides the region passed to load_catalog. When neither names a region:

  • if every region bills the model the same, the result is priced;
  • if a global price applies but some region bills differently, the global price is returned as partial with missing_dimensions=("region",);
  • if only specific regions price the model, the result is unpriced with missing_dimensions=("region",).

A region the model is not sold in is unpriced with nothing missing.

Model registry

from metergraph_core import load_model_registry

models = load_model_registry()
for candidate in models.candidates("gateway"):
    print(candidate.id, candidate.display_name, candidate.pricing_channel)

load_model_registry() reads the packaged models.yaml. Execution profiles describe every route the pipeline can run, while offer groups describe the smaller ordered fields a product exposes for configured credentials. Candidate IDs may intentionally repeat across execution profiles when gateway and Bedrock use different physical routes; each route therefore also has a unique internal key.

A model entry may record released, the publisher's general-availability date as an ISO date. models.release_date(canonical_id, load_catalog()) returns that date with basis released, or, when none is recorded, the earliest effective_from of the model's catalog prices with basis first_price, so a consumer can tell a launch date from a pricing date without computing the fallback itself.

Use validate_model_registry(models, load_catalog()) to verify that every route's provider-facing model ID is declared on its exact pricing channel, resolves to its canonical model, and has an active price on the registry version date. The parser also rejects unknown route providers, provider/channel mismatches on any route, mismatched offer-group credentials/providers, and duplicate physical routes. Pricing remains exclusively in prices.yaml; being priceable does not automatically make a model an approved analysis candidate.

Billing evidence

Servers can pass content-blind, already-extracted gateway fields through the shared trust boundary and combine them with a catalog result:

from metergraph_core import normalize_gateway_evidence, resolve_billing

evidence = normalize_gateway_evidence({
    "gateway": "openrouter",
    "endpoint": "chat.completions",
    "reported_cost_usd": "0.00482",
    "reported_cost_source": "openrouter.usage.cost",
})
decision = resolve_billing(result, evidence)
print(decision.cost_usd, decision.cost_provenance)

The initial qualified contract is OpenRouter Chat Completions. A finite, non-negative openrouter.usage.cost value, including zero, takes precedence over a catalog estimate. The decision retains both values and never adds the separately reported upstream inference cost to the OpenRouter account charge. Unsupported or malformed evidence falls back to the catalog result.

The billing module validates only gateway, endpoint, fixed source names, and decimal cost values. It does not inspect provider response content or own SDK capture, HTTP, timestamps, trace context, persistence, or tenant behavior.

Pricing deltas

A pricing delta proposes changes to a catalog's price rows. It is a change artifact to review and apply, never a catalog to serve.

schema_version: 1
kind: pricing_delta
provenance:
  observations: [openai-api-pricing@2026-10-20]
  evidence: [{source_id: openai-api-pricing, url: "https://...", fingerprint: "sha256:..."}]
operations:
  - op: replace_price
    key: {canonical_id: openai/gpt-x, channel: openai-api, region: global, effective_from: "2026-01-01"}
    expect: {...the full current row...}
    price: {channel: openai-api, region: global, effective_from: "2026-10-20",
            input_per_mtok: "2.50", output_per_mtok: "10", source_url: "https://..."}

Each operation names its row by (canonical_id, channel, region, effective_from) on a model the catalog already has:

  • add_price appends a row; expect is null because no such row may exist. It may not start on the same UTC date as another row for that model, channel and region, even one it does not overlap, because a price id names its row by the UTC date of its start.
  • replace_price closes the open row at the new row's effective_from and appends the new row; expect is the exact current row. The new row must start on a later UTC date than the row it replaces, for the same reason.
  • close_price sets effective_to on the open row without a replacement.

A price id ends with the UTC date of its row's start. If two rows for one model, channel and region would still share an id (possible in a hand-edited catalog), each that does not start at UTC midnight also names its UTC start time, such as 2025-01-02T10:00:00Z.

New rates are decimal strings. apply_catalog_delta(document, delta) returns a new document and never changes either input. It checks every operation's precondition first; if any fails, CatalogDeltaConflict.conflicts names every failing operation and nothing is applied. History is never rewritten, the top-level version and pricing_verified_at are left for the publisher to update, and the complete result is validated with load_catalog_document(). Because preconditions cover only the rows a delta changes, an approved delta still applies after unrelated catalog changes. parse_catalog_delta() validates a delta without applying it.

Source registry

src/metergraph_core/data/sources.yaml lists the official pricing sources the refresh tools read. load_source_registry() loads it, and validate_source_registry(registry, catalog) checks its channels against a catalog. Each entry records:

  • the provider and pricing channel, and whether the catalog carries that channel yet (catalog_channel);
  • the official pricing url, the only address a refresh tool may fetch for it;
  • the parser that reads it;
  • the price dimensions it represents (dimensions): catalog price columns, or GPU-hour and license units;
  • the page's expected structural fingerprint;
  • low_risk: whether automated refreshes may use it (eligible), and the dimensions an eligible source may change.

To inspect the registered source for a provider:

from metergraph_core import load_source_registry

for source in load_source_registry().sources:
    if source.provider == "anthropic":
        print(source.id, source.url, source.parser, source.dimensions, source.low_risk)

Seeding a source does not make it low-risk. Low-risk means a proven official parser whose live structural fingerprint has been reviewed and recorded. Anthropic, Google (Gemini API) and xAI start low-risk; each may change existing rates, in its allowed dimensions, on existing rows.

OpenAI is reviewed discovery only. Its official parser still reads the live rows, but the live pricing data no longer exposes the rendered column header needed to verify the parser's positional columns, so its rows are evidence for review, never automated changes.

Every other source is reviewed discovery only. It is read with a generic parser, html-tables for any page's tables or gpu-license-prose for GPU or license pricing stated in prose, which records normalized evidence and never produces price rows. A generic parser does not mean the page's layout is supported. DeepSeek, Mistral and Perplexity have official parsers (used by scripts/check_prices.py) whose live pages do not currently parse cleanly to the catalog; a source is promoted only through a reviewed registry change once its parser and fingerprint are reviewed. NVIDIA's NIM production pricing is per GPU or license, so it is recorded with GPU-hour and license dimensions and never as token pricing.

Extractors

extract(parser_id, content) reads source content the caller supplies and returns the rates it finds, mapped to catalog price columns, or evidence for a generic discovery parser. structural_fingerprint(parser_id, content) summarizes only the labels, headers and key paths that parser relies on, so a price change or a newly listed model keeps the fingerprint and a format change alters it. Extractors never fetch. The package adds no browser or LLM dependency.

Refresh and discovery scripts

Two repository scripts run the registry against live sources. Neither holds credentials, applies a delta, writes the catalog, publishes, or touches GitHub.

Neither ever emits, stores or redistributes raw fetched page content. Their reports and evidence files hold only structural fingerprints, normalized extracted values (row labels as slugs, parsed amounts, numeric candidates), candidates, operations and short non-verbatim diagnostics. No output may name an input the run read.

scripts/refresh_prices.py takes the registry and a current catalog document. For each low-risk source it fetches the registered URL, checks the page's fingerprint, extracts prices, checks invariants, and prints a JSON report: a pricing delta for the changes that pass, one observation per source (including sources with no change), and every escalation. New models, new regions, new dimensions, rows a page no longer lists, ambiguous parses, format changes, non-positive or missing rates, changes outside the allowed dimensions, duplicated model entries, changes to a row that already has a scheduled end, a row more than one selected source proposes (whether or not they agree), invalid current catalog rates, and backdated or same-day effective dates escalate. A change takes effect on the UTC observation date. It exits 0 with no change, 2 with a delta or an escalation, and 1 on error.

scripts/discover_prices.py scans sources that are not low-risk, the sources behind a refresh report's escalations (--escalations), and new-source content from a file or stdin (--new-source, never a URL). It prints evidence for each: candidate and prior values, the reason for review, normalized page facts, and a proposed delta for changed catalog rows. An optional adapter (--llm-adapter module:callable) is called as propose(source, content, current_rows) and returns delta operations. Its output is kept as review evidence only after it passes schema validation and encodes as strict JSON. Adapter failures and rejected items are reported with stable codes, never with the adapter's own text. Everything discovery emits requires review.

Both read <source-id>.* files from --content-dir instead of fetching. The tests use one synthetic fixture per low-risk source, plus OpenAI's, in tests/fixtures/sources/, reproducing only the structure its parser reads; the generic discovery parsers are tested on small inline synthetic pages.

Provider review and contributions

Model providers, new and existing, can check and improve how MeterGraph prices their models:

  1. Review your entry. Find your provider in sources.yaml (or run the snippet above) and check the registered pricing URL, its parser, and the dimensions it represents. Then check your models' current rows in prices.yaml.
  2. Propose a correction or a new source. Edit your entry or add a new provider with its official pricing URL. A new source starts non-low-risk.
  3. Add the extractor, fixture and tests when proposing low-risk status. A discovery-only source needs no fixture of its own. A source proposed for low-risk status needs an extractor in extractors.py and a minimal synthetic fixture in tests/fixtures/sources/ that reproduces only the structure the parser reads, never a saved copy of the page, with tests.
  4. Open a pull request with the official-source evidence and an effective date for each pricing change. Price changes close the old row's window and add a new row; they never edit history.

Provider-submitted changes go through the same validation and review as any other catalog change. A provider cannot mark its own source low-risk or publish directly to the live catalog: low-risk status changes only through a reviewed registry change, and publication is done by MeterGraph's release process.

Public API

from metergraph_core import (
    Alias,
    BillingDecision,
    CatalogDeltaConflict,
    CatalogDeltaError,
    CatalogError,
    CatalogSnapshot,
    CostResult,
    GatewayBillingEvidence,
    LoadedCatalog,
    ModelRegistry,
    ModelRoute,
    ModelUsage,
    Price,
    PriceResult,
    PriceRowKey,
    PriceSource,
    RefreshResult,
    ResolvedPrice,
    SourceRegistry,
    apply_catalog_delta,
    discovery_evidence,
    extract,
    load_catalog,
    load_catalog_document,
    load_model_registry,
    load_source_registry,
    normalize_gateway_evidence,
    parse_catalog,
    parse_catalog_delta,
    plan_low_risk_refresh,
    resolve_billing,
    structural_fingerprint,
    validate_model_registry,
    validate_source_registry,
)

Catalog maintenance

The two manually maintained public datasets live under src/metergraph_core/data/. Update models.yaml when model identity, display, routing, or managed candidate policy changes. Update prices.yaml when rates or billing rules change. sources.yaml changes through reviewed pull requests, as described in Provider review and contributions. Price corrections close or add effective windows; they never rewrite historical prices in place. Either data change increments its own declared version and produces a patch release of metergraph-core. Software and data versions remain separate because they have different lifecycles.

The model registry version is an ISO date. When more than one registry ships on the same date, append a positive revision such as 2026-09-25.2 so consumers can distinguish the policies while pricing validation still uses that date.

Metadata

Release files for metergraph-core 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for metergraph-core 0.4.0
File Size Uploaded Provenance
metergraph_core-0.4.0.tar.gz 86.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for metergraph-core 0.4.0
File Interpreter ABI Platform
metergraph_core-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 172.2 kB

Release files / metergraph_core-0.4.0.tar.gz

Download URL metergraph_core-0.4.0.tar.gz
Size 86.8 kB
Tags Source
SHA-256 checksum
How to use checksums
66d6f1d8140cfd5436cd5d649d152899b660686f227a1370d4a8b5910bd352c6
BLAKE2b-256 checksum
How to use checksums
33db7e09ea4f249dba5bb0b82e5d597c925c89442f44d8e842bf75da43d10e80
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / metergraph_core-0.4.0-py3-none-any.whl

Download URL metergraph_core-0.4.0-py3-none-any.whl
Size 85.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
07d7b6eb0a10be6c0208b61e3397b14d138cf145afb9b022a6f9155e0630d4ce
BLAKE2b-256 checksum
How to use checksums
be3ce11e7efc73665781d7f5605e45f7cf141d1505a45d14008523ee87d9a86b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for metergraph-core 0.4.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for metergraph_core-0.4.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page