How a request is metered
This page explains the per-request numbers behind the request feed and the per-turn grouping: the token counts on a row, the cost beside them, and the event that produced both.
What it is
Metering is the path from an admitted request to one priced row. Your request is relayed to the provider on your own key and the response streams back untouched. After the last byte we read the provider's own usage figures out of a bounded window of that response, price them against a frozen list, and emit one content-free event. Ingestion turns that event into a receipt. Every number on the row is either something the provider reported or integer arithmetic over it. Nothing is inferred: a count the provider did not report is stored as null, and a model we cannot price is stored with a null cost.
How it is computed
1. Admission decides first. Only an admitted request is metered. An unknown key (401), a throttle (429) and a cap refusal (402) all return before the provider is contacted, so they produce no row, no token count, and no cost. See limits and budgets.
2. The tee. Provider bytes pass through to you untouched while a bounded copy is held in memory for the life of one response. Only a 2xx response is copied. The window is at most 256 KiB from the head and, for a server-sent-event stream, the last 32 to 64 KiB. A non-streaming completion body fits inside the head. On a stream the head holds the opening frames — which name the served model, and on Anthropic carry the input and cache counts — and the tail holds the closing frames, where the final usage figure and the terminal reason sit. Bytes between those ends pass unobserved. The extractor borrows the copy, returns integers plus two provider identifiers (the model actually served and the finish reason), and the bytes are dropped with the response. Nothing from it is stored or logged.
3. What the provider said. Token counts are read by field from the provider's own usage object, never counted by us:
| Count | OpenAI | Anthropic |
|---|---|---|
| Input | prompt_tokens, or input_tokens on the Responses shape | input_tokens |
| Output | completion_tokens, or output_tokens | output_tokens |
| Cached input | prompt_tokens_details.cached_tokens, or input_tokens_details.cached_tokens | cache_read_input_tokens |
| Cache write | not reported | cache_creation_input_tokens |
| Reasoning | completion_tokens_details.reasoning_tokens | not reported |
On a stream the frames merge per field. For OpenAI the last frame carrying a usage object wins, at the frame root for /v1/chat/completions and nested under response for /v1/responses. For Anthropic message_start supplies the input and cache counts and the final message_delta supplies the cumulative output count. A torn frame at either edge of the window is skipped, never repaired. A body that does not parse, or a stream whose usage frame fell outside the window, yields nothing at all rather than a guess.
4. The event. After the last byte the gateway emits exactly one metering event, fire and forget. Every field is an id, a count, a timing, a status, or a closed enum; the shape has no field capable of holding a prompt, a completion, a header value, or a raw path. Emitting takes a bounded in-memory lock and never waits on IO, so metering can lose a reading but can never delay or fail your request. Overflow drops the oldest buffered events and counts every drop. While ingestion is unreachable, events spill to a size-bounded local disk ring and replay from it; readings evicted past that bound are counted too.
5. Pricing. Ingestion prices the event against the frozen price list pinned on the receipt as refPriceVersion — today rpl-2026-08-05, 51 rows across both providers, compiled into the binary and never fetched at runtime. A published version never changes; a correction ships as a new version. Rates are micro-USD per million tokens, and 1,000,000 micro-USD is one dollar.
Find the row for this (version, provider, model), where the model is the one the provider says it served. Sum four products in 128-bit integers — full-rate input times the input rate, cached input times the cached rate, cache-write tokens times the write rate, output tokens times the output rate — then floor-divide by 1,000,000 exactly once, at the end, so per-component truncation cannot accumulate.
The two providers count cached input differently and we follow each one's own convention rather than normalizing them:
- OpenAI reports
cached_tokensinsideprompt_tokens, so full-rate input isprompt_tokens - cached_tokens. OpenAI rows carry a zero cache-write rate, because OpenAI reports no cache-write tokens. - Anthropic reports
input_tokensexcluding both cache reads and cache writes, so those two counts are added on top of it, the write at the provider's published premium rate. - Reasoning tokens are not a term in that sum. Both providers report them inside the output count, so they price at the output rate exactly once.
- Detail the provider did not report contributes zero, because it is absent from that provider's invoice too. The receipt keeps the field null rather than a zero.
Three things produce a null cost, each for a stated reason: a model absent from the list, which is alerted internally as price_list_mismatch; usage the provider never reported; and a coalesced deduplication follower, which made no provider call and so has no provider charge. A null-cost row counts as one request, adds zero spend, and does not chain.
Worked example
An Anthropic claude-sonnet-4-5 request under rpl-2026-08-05: input 3,000,000, cached input 300,000, cache write 3,750,000, output 15,000,000 micro-USD per million tokens. The provider reported 2,000 input tokens, 1,000 cache-read tokens, 800 cache-creation tokens and 400 output tokens.
| Term | Tokens | Rate (micro-USD/MTok) | Product |
|---|---|---|---|
| Input, not reduced by cache reads | 2,000 | 3,000,000 | 6,000,000,000 |
| Cache read | 1,000 | 300,000 | 300,000,000 |
| Cache write | 800 | 3,750,000 | 3,000,000,000 |
| Output | 400 | 15,000,000 | 6,000,000,000 |
The sum is 15,300,000,000, which floor-divides to 15,300 micro-USD. The row stores that integer; the feed renders $0.02, rounded for display only, never for storage. Had the same counts come from OpenAI, the input term would have been taken on 2000 - 1000 tokens instead, because there the cached tokens sit inside the prompt count.
6. What comes back. Two counters are corrected after the response, both off the request path. The rate limiter replaces its admission estimate with the provider's reported input plus output tokens. The cap counter replaces that same estimate's money value with the priced cost, keyed to the period the request started in, so a reconcile landing after a month boundary corrects the month it belongs to. An unpriced request reconciles neither: the estimate stands rather than being replaced by a guess.
What it does not include
- Prompts, completions, headers, secrets. No metering field can hold one.
- Requests that never reached the provider. Refusals and throttles are not metered and never billed.
- Traffic that did not pass through the gateway.
- Deduplication followers, which made no provider call and carry a null cost.
- Anything on your provider invoice that is not list price: discounts, committed-use, credits, free tiers, minimums, taxes.
- Rate dimensions outside the price schema — long-context tiers, one-hour cache writes, regional uplifts, fast-mode SKUs — which price at the base rate. That under-measures and never over-charges.
- Your Recovea subscription.
Limits and current status
- The tee is bounded, so extraction can honestly fail. A usage frame past 256 KiB of head and outside the trailing window is not seen, and the receipt then carries null tokens and a null cost. An error response is never teed, so a failed call is metered with its true upstream status and no usage.
- Dropped readings are counted, but not shown to you. The drop counter lands in our own metrics. No customer surface displays it today.
- The feed renders nulls as absent. A count the provider never reported and a cost nothing could price reach the wire as null and render as
-. The read path used to coerce both to0before serialization, so a feed row could show0where the stored receipt held null; that coercion is gone from the feed row, the receipt body and the per-turn view, and the stored row, the export and the rendered JSON now agree. A measured0is still0, because that one is a reading. The export carries integer counts and an integer cost, because those are the exact values a row hash was derived over. - Per-turn grouping has nothing to group. The deployed read path never populates a turn tag, so every row lands in
ungroupedand the payload says so plainly. No turn is ever invented. - Two display fields are placeholders. In the deployed read path
projectalways rendersdefaultandenvalwayslive, and the key column is the key id, not a masked key. Project and tag attribution ride separate tables, not the receipt row. See attribution. - The price clock is manual. A
price-watchjob fetches published provider pricing, diffs it against the shipped rates, and opens proposals; publishing is a reviewed commit of a new version, never an edit of an old one. That job is not in the deployed schedule set today, so it runs when we run it. One dated caveat inside the current list: theclaude-sonnet-5rows carry introductory pricing that ends 2026-08-31, so a successor version has to ship before 2026-09-01. - Two layers hold the same spend. The receipt commits in one transaction and the daily rollup in a second. A rollup failure after the receipt lands leaves that spend out of the dashboard total while the receipt keeps it, and no reconciliation job repairs the gap today. Receipts are the record of truth. See spend.
- Counterfactual. Measured, never applied. No lever is activated in any deployed environment, so the served model always equals the baseline and no request coalesces today.
- Cost per successful output is served at list price; invoice reconciliation at period close is planned, not wired.
- The Recovea Score engine is not shipped. No workspace has a score today.
Where to check us
- Take a
requestIdfrom the request feed and find the same row in the period export:GET /platform/v1/ledger/export?period=YYYY-MM&format=jsonl. Each row carriestokensIn,tokensOut,tokensCached,costMicroUsd,priceListVersion,previousHashandrowHash. - Compare those token counts against the provider's own figures for that request, from your provider dashboard or your own client logs. They should be identical: we copy them, we do not compute them.
- Re-derive the cost with the four-term sum above at the rates for the pinned
priceListVersion. Those rates are the providers' own published list prices as of the version date, so you can check ours against theirs. No endpoint serves the rate rows yet, which is a gap. - Re-derive the row hash to confirm nothing changed after the fact. The recipe is
recovea-chain-v1, on the ledger page. Note that the hashed provider label isopenaioranthropic, lowercase and unseparated, even where a JSON encoding spells itopen_ai.
If your arithmetic and ours disagree, send us the request id. We publish the correction as a new row, never as an edit.