Skip to navigation

Catalog, cost models and overrides

Pick the models you pay for from the provider price catalog, layer your own rates on top, and add models the catalog lacks.

This guide covers the first half of pricing from cost: where provider prices come from, how a cost model records what you pay, and how your own rates win over the catalog.

Every amount in the examples is illustrative, and so are the providers (Vidlab, Textco) and the catalog entries shown: none is a real provider price.

Before you start

The provider price catalog

The catalog is a list of provider models and their prices that Runledger maintains. It is read-only: you can’t add to it or change it. It is the same in the sandbox and in live. Reading it needs config:read.

ToCall
List the providersGET /v1/catalog/providers
Find a model by provider, kind or unitGET /v1/catalog/models?provider=vidlab&unit=video_seconds
Read one model with the price version in forceGET /v1/catalog/models/{catalog_model}
Read price versions, including scheduled ones, and what changed since you last lookedGET /v1/catalog/prices?changed_since=2026-10-01T00:00:00Z

A catalog model declares what it is metered in, with who controls each unit and dimension, as a cost model does. This is an invented entry:

{
"id": "catm_5xT7qD2mKa",
"object": "catalog_model",
"provider": "vidlab",
"model": "vidlab-v2",
"name": "Vidlab v2 video",
"kind": "video",
"status": "active",
"units": [{ "unit": "video_seconds", "controlled_by": "customer" }],
"dimensions": [
{ "key": "resolution", "controlled_by": "customer", "values": ["720p", "1080p"] }
],
"current_price": "catp_4dK2mZ8vQw",
"current_price_version": 1,
"last_verified_at": "2026-10-24T09:00:00Z",
"updated_at": "2026-10-20T04:12:00Z",
"price": {
"id": "catp_4dK2mZ8vQw",
"object": "catalog_price",
"catalog_model": "catm_5xT7qD2mKa",
"version": 1,
"status": "active",
"currency": "usd",
"rates": [
{ "unit": "video_seconds", "unit_amount_decimal": "6", "dimensions": { "resolution": "720p" } },
{ "unit": "video_seconds", "unit_amount_decimal": "12", "dimensions": { "resolution": "1080p" } }
],
"effective_from": "2026-10-20T04:12:00Z",
"as_of": "2026-10-20",
"source_url": "https://vidlab.example/pricing",
"source_reference": "Vidlab pricing page, read 20 October 2026",
"published_at": "2026-10-20T04:12:00Z",
"last_verified_at": "2026-10-24T09:00:00Z"
}
}

Amounts are decimal strings in minor units of the price’s currency, as everywhere else in the API: "12" is 12 US cents for each second of 1080p video. The units, dimensions, per and tiers work as described below.

What every entry carries. A source (source_url and source_reference), the date it takes effect (effective_from), the date Runledger read it (as_of), and when Runledger last checked it against its source (last_verified_at). A published price version never changes. A price change, or a correction, is a new version with its own effective_from, never earlier than the day it is published, and a changes list of what differs from the version before. A model the provider has retired becomes deprecated: cost models that use it keep working with the last version, and no new versions are published.

Freshness target. Runledger commits to these targets for the providers and models the catalog covers:

What happensTarget
A provider publishes a price changeThe catalog has a new price version for it within 1 business day of the provider publishing it. The version carries the provider’s own effective_from when that date is still ahead, so a change announced for next month is in the catalog, scheduled, long before it applies. A change already in effect takes effect when Runledger publishes the version, never from an earlier date
A covered provider adds a modelThe model is in the catalog within 5 business days
Any entryCarries source_url, effective_from and last_verified_at, so you can see where a price came from and how recently it was checked; Data sources and licences says how the checking works

A business day is Monday to Friday in Sydney time (Australia/Sydney). There is no public holiday calendar, so a public holiday counts as a business day. The clock starts when the provider publishes the change or the model, not when Runledger notices it, and published_at on a catalog price shows when Runledger published the version.

These are freshness targets, a commitment about how quickly the catalog follows providers. They are not a guarantee that a price is correct, and there is no service-credit SLA: missing a target gives no automatic credit or refund. Three things stay true however fresh the catalog is:

  • Your overrides always take precedence. An override beats the catalog price on every line it matches, so a negotiated rate is never replaced by a catalog update.
  • Recorded usage is never repriced. A new version applies from its effective_from onward, and a correction takes effect from the date it is published. Costs and events already recorded keep the version that priced them. A rate card you add to a custom cost model must start after every earlier version and can’t start in the past.
  • The invoice and the contract are what you pay. The catalog says what the source said on the as_of date. Where your bill differs, fix your numbers with an override.

What it covers. A stated set of providers and models, not every provider. GET /v1/catalog/providers lists the providers covered and how many models each has, and GET /v1/catalog/models lists the models. Those lists are the statement of coverage: the targets above apply to what is listed, and a provider or model that isn’t listed isn’t covered and needs a custom cost model. Coverage includes units other than tokens (images, video seconds, audio seconds, GPU or session time) where providers publish a price. When trace() names a catalog model you haven’t set up, the call is priced from the catalog’s list price (priced_by: price_table), without overrides, and carries the warning catalog_model_available, which names the model to adopt so your own rates apply.

Cost models and rate cards

A cost model names a provider and model the way trace() does, declares its units and dimensions, and carries rate cards. It gets all of that in one of two ways, shown by its source:

  • catalog. You send catalog_model, and Runledger takes the provider, model, kind, units, dimensions and rates from the catalog. The model follows the catalog’s price versions: each one becomes a rate card of the cost model, with the same rates and effective_from.
  • custom. You declare the model and enter the rate cards yourself, for a model the catalog doesn’t have or rates the catalog can’t know.
{
"key": "vidlab-v2",
"name": "Vidlab v2 video",
"catalog_model": "catm_5xT7qD2mKa"
}

That is the whole setup for a catalog model. The response has source: "catalog", the provider and model, and the active rate card. A custom cost model looks like the catalog entry above with a rate_card of its own, here for a self-hosted model that is billed by GPU second:

{
"key": "selfhost-llm",
"name": "Self-hosted LLM, GPU time",
"provider": "selfhost",
"model": "llm-70b",
"kind": "compute",
"units": [{ "unit": "gpu_seconds", "controlled_by": "provider" }],
"rate_card": {
"currency": "usd",
"as_of": "2026-10-20",
"source_reference": "Cluster cost for October 2026 divided by GPU seconds used",
"rates": [
{ "unit": "gpu_seconds", "unit_amount_decimal": "0.35" }
]
}
}

"0.35" is 0.35 of a cent for each GPU second. A custom cost model’s rate cards are yours to add (POST /v1/rate_cards). A catalog cost model refuses them with 409: its rates come from the catalog, and your changes go in overrides.

Units. A unit is a plural name for what you count. The names to use are input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, images, video_seconds, audio_seconds, gpu_seconds, session_hours, requests and searches. Any other plural works, such as tool_calls.

Choosing a rate. A usage line has a unit, a quantity and dimension values. Runledger picks the rate for that unit whose dimensions all match the line. A rate that leaves a dimension out matches any value, and the most specific match wins. A line with no matching rate is recorded without a cost and with the warning rate_not_found, so a gap in the catalog shows up instead of a zero.

Per and tiers. Providers quote token prices per million, so a rate has a per: "per": "1000000" with "unit_amount_decimal": "300" is 300 cents per million tokens. A rate can have tiers instead of one amount. In graduated mode each band’s price covers the quantity inside it. In volume mode the band the whole quantity falls in prices all of it, which fits a provider that charges more for every token of a long prompt:

{
"unit": "input_tokens",
"per": "1000000",
"tier_mode": "volume",
"tiers": [
{ "up_to": "200000", "unit_amount_decimal": "300" },
{ "unit_amount_decimal": "600" }
]
}

A tier applies to one call’s quantity. Tiers across a customer’s usage over a month aren’t supported.

Your own rates override the catalog

Some customers, especially larger ones, don’t pay list price. An override is your own rate for a catalog cost model, and it always wins over the catalog:

{
"key": "textco-large",
"name": "Textco Large",
"catalog_model": "catm_2hK8vP5nRc",
"overrides": [
{
"key": "textco_negotiated",
"discount_percent": "0.2",
"category": "negotiated_discount",
"effective_to": "2026-12-31T23:59:59Z",
"reason": "Negotiated 20% off to 31 December 2026"
},
{
"key": "textco_private_output",
"unit": "output_tokens",
"unit_amount_decimal": "1100",
"per": "1000000",
"category": "private_deployment",
"effective_from": "2026-11-01T00:00:00Z",
"reason": "Provisioned deployment, order ref PO-1187"
}
]
}
  • discount_percent takes that share off the catalog rate of every matching line, tiers included. "0.2" is 20% off.
  • unit_amount_decimal replaces the catalog rate for a unit, so it covers a private or provisioned deployment, a committed-spend rate you already know, and a unit the catalog model doesn’t list, which is how you extend a catalog model.
  • unit, dimensions, effective_from and effective_to say where and when it applies. With none of them it applies to every line of the model from the moment it is saved. category is a label for reporting, and reason goes in the audit log.

Precedence. For each usage line, at the line’s own timestamp:

  1. An override in force that matches the line’s unit and dimensions. If several match, the most specific wins: one that names a unit and dimensions beats one that names only a unit, which beats one that names neither. Two equally specific overrides whose dates overlap are a validation error, refused with 400 and config_invalid, so there is never a tie to guess at.
  2. Otherwise the catalog price version in force.
  3. Otherwise no rate: the line is recorded without a cost and with rate_not_found.

A custom cost model has no catalog layer: its own rate card in force is used, and it takes no overrides.

The cost on a line says which one priced it, so a cost can be traced to the rate behind it (see cost attribution). Say the catalog lists Textco Large at 300 cents per million input tokens (up to 200,000 tokens) and 1,500 per million output tokens. A call with 1,840 input tokens and 512 output tokens costs 0.552 + 0.768 = 1.32 at list price, and 1.32 × 0.8 = 1.056 with the first override above.

Overrides record the rate you pay. They don’t track spend against a commitment, so a rate that changes when you pass a spend threshold is entered as the new rate from the date you pass it. Overrides change only costs from their effective_from: nothing already recorded is repriced.

Next steps