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.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
Already in the catalog? Just fix the number
Editprices/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 buildand 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:- 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
idand throughfind_modelbefore 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.
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 underprices/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.- S2S uses the LLM shape with
input_audio_mtokandoutput_audio_mtokset. 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_kminutesfiles 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 inprice_commentsexactly 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_OVERRIDESinprices/src/prices/build_docs.pyor it lands on the STT tab. Say so in your issue and the maintainer will add it.
Discuss first
Find the rates
rate_per_min * 1000 / 60 and record the source rate in price_comments.Credit-billed TTS converts the same way: ElevenLabs Creator is 0.00022 per credit) and Turbo v2.5 burns 0.5 credits per character, so input_kchars = 0.00022 * 0.5 * 1000 = 0.11.Copy the template
prices/providers/<your-provider>.yml. The filename is lowercase and hyphenated, matching the provider’s common name.Fill in provider metadata
name, id, pricing_urls, api_pattern, model_match, and provider_match. Metadata is identical across modalities.Add the models
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.Add voice multipliers (TTS only)
default is required, and multipliers need input_kchars (or output_audio_kseconds) to scale.Build and verify
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.Open the PR, with a screenshot
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.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
Runmake 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:
Troubleshooting
LookupError: Unable to find provider matching '<model-ref>'
LookupError: Unable to find provider matching '<model-ref>'
model_match clause does not cover the model ID the user passed. Update the prefix or regex and rebuild.Models are not sorted by ID
Models are not sorted by ID
models list by id. The error message tells you exactly which entry to move and where.My provider bills in credits, tokens, or minutes
My provider bills in credits, tokens, or minutes
price_comments, so consumers on other tiers can derive their own rate. Which tier to convert from is answered below.My provider has several pricing tiers. Which one?
My provider has several pricing tiers. Which one?
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 vendor is running a promotion. List price or discounted price?
The vendor is running a promotion. List price or discounted price?
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.voice_multipliers are not supported for STT
voice_multipliers are not supported for STT
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.The provider's published numbers do not reconcile
The provider's published numbers do not reconcile
price_comments, price the rate you can defend, and say in the PR which figures disagreed. An honest note beats a confident wrong number.