Skip to main content
A price gets into this catalog one of four ways. On day one they all produce the same number. They differ on day ninety, in whether anything notices when the vendor changes it. Paths 1 and 2 earn the model an api marker in the tables on this site. Paths 3 and 4 do not, because nothing is watching the source between visits.
“Caught automatically” means detected, not applied. No path in this project updates a published price on its own. Polling and importers report that a rate has moved; a maintainer confirms it against the vendor and merges the change, and that human is the only thing that sets prices_checked. The difference a feed buys you is that drift surfaces the next day instead of whenever somebody happens to look. How Fresh? explains why the rule exists.
There is a fifth thing that looks like a path and is not. LLM rates are cross-checked against Helicone, OpenRouter, LiteLLM, and Simon Willison’s llm-prices, downloaded into a gitignored prices/source_prices/. Those feeds exist to flag disagreement with a rate we already hold. They never set one. Please do not open a PR that copies a number out of an aggregator.
Every price in this catalog is US dollars, and we do not convert. A vendor that publishes only in another currency is out of scope for now, however good the data is: an exchange rate moves daily, so a converted price would be wrong within a week of being written and there would be no honest date to put on it. If a vendor publishes a USD page alongside a local one, use the USD page and say so. If they publish only CNY, INR, KRW or similar, open an issue describing what they publish rather than a PR converting it. A pricing endpoint declaring a non-USD currency is rejected outright for the same reason.

Hand it to a coding agent

Most of path 4 is mechanical: read a page, convert a unit, write YAML in a shape that already exists a dozen times over in this repository. Claude Code, Cursor, Codex and friends are good at exactly that. Clone the repo, open your agent in it, and paste this.

Add a provider's prices to voice-prices

Open in Cursor
An agent will produce a confident, well-formatted, plausible YAML file whether or not it read the page correctly. The last step exists for that reason: it has to tell you what it skipped and what was ambiguous, and you have to check the rates against the page yourself before opening the PR. make check-contribution catches unit errors and missing provenance, not a number that was simply read off the wrong row.

Already in the catalog? Just fix the number

Edit prices/providers/<provider>.yml. Set prices_checked on the model to today’s date (why it matters), add or update price_comments explaining the change with a reference URL, then run make build to regenerate the JSON files. Pre-commit does this for you if you ran make install. Please do not:
  • edit any JSON file directly: they are all built with make build and are compact by design
  • add verbose descriptions to providers or models, we only need enough detail to give the end user a rough idea of the model’s capabilities
  • try to change the schema of providers or models without creating an issue to discuss the changes first
  • add new providers without creating an issue to discuss the changes first, adding models is fine

1. Serve a live pricing endpoint

The best outcome for everyone. You publish your current rates at one HTTPS URL, generated from whatever your billing system treats as the source of truth. A scheduled job polls it daily, so the day a rate moves we know, instead of finding out months later when someone re-reads your pricing page. A maintainer still confirms and merges the change; what disappears is the part where nobody notices. The full spec, the meter and unit grid, and a worked example are on Serve a pricing endpoint. If you work at a provider, start there and ignore the rest of this page.

2. Read the provider’s pricing API

Some providers already serve machine-readable rates without knowing it: a public models endpoint that happens to carry prices. When that exists, we write an importer instead of transcribing by hand. Two are in the tree and either is a fine thing to copy: An importer has four obligations, all of them learned the hard way:
  • Refuse rather than guess. If the API disagrees with itself, for example a tier rate that contradicts the headline rate, write nothing for that model and report it. A refused model is a question for the vendor, not a number to average.
  • Never clobber a human. Look the model up both by id and through find_model before adding it. A model can be keyed by one id and routed by another, and checking only one of the two silently creates a duplicate row.
  • Report what vanished. Models that used to be in the API and no longer are must be surfaced as retired. A quietly dropped model is the failure that survives longest, because nothing about it looks broken.
  • Support DRY_RUN=1. The weekly job runs every importer read-only. In a dry run, a model that is waiting to be added still counts as outstanding work, so exit non-zero for it.
Add a make <vendor>-get target alongside the others. Models an importer writes land as provenance: source: imported with api_backed: true and no prices_checked. That is deliberate: the importer proves the number is current, not that a human has agreed with it. Setting prices_checked stays a human act.

3. Snapshot a structured rate card

Sometimes a provider publishes its entire rate card in one structured file, but there is no stable URL worth polling. Commit the snapshot under prices/sources/ and write a generator that rebuilds the YAML from it. prices/src/prices/livekit_gen.py does exactly this: it regenerates both livekit.yml and livekit-scale.yml from prices/sources/livekit_pricing.json, and make livekit-get reruns it. When the vendor changes prices you replace the snapshot and rerun one command, and the diff shows precisely which rates moved. That is the whole benefit over path 4: the update is mechanical and reviewable rather than a hand edit across dozens of rows.

4. Read the pricing page by hand

The common path, and the one most vendors need. It is also the one that goes stale, so the audit trail matters more here than anywhere else: every priced model carries the source URL, the conversion arithmetic, and the date a human read the page. Three categories have no template of their own because they reuse a priced shape above:
  • S2S uses the LLM shape with input_audio_mtok and output_audio_mtok set. A model is filed as speech-to-speech when it prices audio in both directions, so nothing extra is needed.
  • Agents is for platforms selling one bundled per-minute price covering STT, LLM, TTS and orchestration together. Setting agent_kminutes files a model there automatically. Do not also set the component fields: if a platform publishes both a bundle and its parts, the bundle is what it charges, and the parts belong in that vendor’s own entries. State in price_comments exactly what the bundle includes and what it excludes, because most platforms bill the model or telephony at cost on top.
  • VAD uses the STT shape, since both bill per second of audio. That makes it indistinguishable from STT by price alone, so a VAD model also needs an entry in MODALITY_OVERRIDES in prices/src/prices/build_docs.py or it lands on the STT tab. Say so in your issue and the maintainer will add it.
Paths 2 and 3 produce the same YAML. They just write it from a script instead of by hand, so the shape below is worth reading whichever path you are on.
1

Discuss first

Open an Add a provider issue with the provider name, pricing page, models, and modality. This catches duplicates and naming conflicts before you write YAML.Looking for something to pick up? LiveKit plugin coverage lists every vendor a LiveKit agent can call and which of them have no rate here. The rows marked “not investigated yet” are the open ones: nobody has checked whether that vendor publishes a per-unit rate.
2

Find the rates

You need the per-unit rate for each model, any tier or mode differences (premium voices, streaming versus batch, context tiers), and a deep-linked source URL per model.Most STT providers quote per minute. Convert with rate_per_min * 1000 / 60 and record the source rate in price_comments.Credit-billed TTS converts the same way: ElevenLabs Creator is 22/monthfor100,000credits(22/month for 100,000 credits (0.00022 per credit) and Turbo v2.5 burns 0.5 credits per character, so input_kchars = 0.00022 * 0.5 * 1000 = 0.11.
A rate quoted by a reseller is not evidence for the vendor’s own rate. Gateways sometimes sell below a vendor’s public self-serve tier, not just above it, so a resold number is neither an upper nor a lower bound. Price the direct vendor from the vendor’s own page or leave the entry out.
3

Copy the template

Copy the template for your modality to prices/providers/<your-provider>.yml. The filename is lowercase and hyphenated, matching the provider’s common name.
4

Fill in provider metadata

Set name, id, pricing_urls, api_pattern, model_match, and provider_match. Metadata is identical across modalities.
model_match routes a bare model_ref (no provider_id) to your provider. If its prefix or regex does not cover your model IDs, consumers get LookupError: Unable to find provider matching '<your-model>' even though your YAML is in the catalog.
5

Add the models

The models list must be sorted alphabetically by id: the Pydantic validator enforces it. Every model with a voice priced field needs pricing_source_url, and prices_checked is the date you verified that URL. Prefer match: equals: <id> so a model_ref resolves to exactly one row or fails loudly.
6

Add voice multipliers (TTS only)

Skip this unless your TTS provider bills premium or cloned voices at a different per-character rate. default is required, and multipliers need input_kchars (or output_audio_kseconds) to scale.
7

Build and verify

Run make build then make test. Smoke-check routing in a REPL: calc_price(Usage(characters=200), model_ref='murf-standard') should return your price and your provider id.
8

Open the PR, with a screenshot

Commit the YAML plus the regenerated prices/data*.json, the schema files, and packages/python/voice_prices/data.py. In the PR body, give the pricing URL you verified against, the date you verified it, and any quirks: voice tiers, streaming or language splits, non-standard billing units, deprecated models you excluded.Attach a screenshot of the pricing page showing the rates you added. CI requires one on any PR that adds a hand-read rate. It is a low bar on purpose: it does not prove the conversion is right, it proves someone loaded the page. Rates written by an importer are exempt, because no human read a page for those. Crop it to the rate row or table you read, not the whole page. The evidence is the number and its label; the rest of the page is the vendor’s copyrighted layout and we do not need to republish it to prove the rate was there.
Attach it when you open the PR, not after. CI reads the PR body out of the event payload that triggered it, and pull_request does not fire on a body edit. So adding the screenshot to a PR that already failed changes nothing on its own, and re-running the job replays the same stale payload. Push a commit to re-trigger, or open the PR with the screenshot already in it.
Upload the file: drag it into the PR body so it lands on github.com/user-attachments. An image linked from anywhere else does not count, because linking someone else’s URL costs nothing and proves nothing about whether you opened the page.

What CI checks, and what it cannot

Run make check-contribution before you push. It runs on every PR, including from forks, and it looks at exactly the models your branch adds or reprices: None of that tells you a rate is correct. It cannot: a fork PR has no secrets, so no browser and no model can run against it. What it does is refuse the shapes a wrong rate takes. Correctness is checked separately, by a maintainer running the freshness verifier, which renders the page and requires two independent agents to agree before it will report a rate as matching.
The bands are deliberately loose, about ten times outside the widest rate in the catalog. If your genuinely-new price point trips one, that is worth saying in the PR rather than working around: it usually means a vendor has repriced a whole category.

Troubleshooting

Your provider-level model_match clause does not cover the model ID the user passed. Update the prefix or regex and rebuild.
The validator enforces alphabetical sorting of the models list by id. The error message tells you exactly which entry to move and where.
Convert to the native priced field and document the conversion in price_comments, so consumers on other tiers can derive their own rate. Which tier to convert from is answered below.
The cheapest tier a new account can reach with no spend commitment. Usually “Pay As You Go” or “On-Demand”. Name it in the provider’s pricing_tier field, worded as the vendor words it, so a reader can find the same words on the pricing page and check the number against their own invoice.Where a vendor sells no such tier, take the cheapest paid plan and say so: Cartesia’s Pro at $4/month, ai-coustics’ Startup, because neither sells single units. Where a vendor publishes one price with no plans to choose between, write the literal Single published rate.Never take a committed or negotiated tier. Deepgram’s Growth is about 12.5% cheaper but costs $4,000/year to enter, so a catalog quoting it would understate every self-serve user’s bill.CI fails a PR that adds or reprices a model on a provider with no pricing_tier.
The discounted price, with the list price recorded in price_comments.Deepgram currently renders Current price $0.0048/min beside Regular price $0.0077/min. The catalog answers “what would I pay today”, so it carries $0.0048. Taking the list price would be wrong for every user for the entire promotional period, and the freshness check catches the expiry as drift.This is the single most common error in voice pricing. At least two other public pricing datasets publish Deepgram Nova-3 at the regular price, overcharging it by 60%, and this catalog did too until it was traced back to a two-price cell.
Setting voice_multipliers on a model priced by input_audio_kseconds fails validation by design. If your provider charges different rates by language, ship separate model entries with distinct IDs, the way Deepgram does with nova-3 and nova-3-multilingual.
Do not average them and do not pick the friendlier one. Record what you saw in price_comments, price the rate you can defend, and say in the PR which figures disagreed. An honest note beats a confident wrong number.
Advanced LLM pricing (tiered rates, conditional daily windows, cache-write, usage extractors) and the complete FAQ live in the full contributing guide.