Costs
The usage.cost breakdown, how the prepaid hold works, and why a request can be refused with 402.
Every successful response tells you what it cost:
"usage": {
"prompt_tokens": 18,
"completion_tokens": 42,
"total_tokens": 60,
"cost": {
"input_tokens_cost": 0.00009,
"output_tokens_cost": 0.00042,
"total_cost": 0.00051
}
}All figures are USD. total_cost is what came off your prepaid balance.
Fields
| Field | Meaning |
|---|---|
input_tokens_cost | Cost of the prompt tokens. |
output_tokens_cost | Cost of the generated tokens. |
reasoning_tokens_cost | Cost of reasoning tokens, on models that bill them separately. |
citation_tokens_cost | Cost of citation tokens, on models that produce them. |
search_queries_cost | Cost of provider-side search queries, where applicable. |
request_cost | Any flat per-request charge. |
total_cost | The sum. This is the authoritative number. |
cost is present when the serving provider supports cost calculation. usage itself can
be null when a provider reported no usage at all, so guard for that before reading cost.
Reserve, then settle
ModelBeat is prepaid, so it cannot discover after the fact that you could not afford a call. For each request it:
- computes the worst-case cost, bounded by the specific models that could serve this request;
- reserves that amount against your balance;
- routes and executes;
- settles the reservation against realized usage, releasing the difference.
You are charged what the call actually cost, not the hold. The hold exists only for the duration of the request.
Why you might see 402
{ "error": "modelbeat: insufficient balance" }The hold exceeded your available balance, which is not necessarily the final cost. Because the
hold is a worst case, a large max_tokens can be refused even when the call would have
been cheap in practice.
Two ways out:
- Top up. Obvious, and correct if your usage is growing.
- Lower
max_tokens. This directly shrinks the hold. If you know a reply will be short, saying so lets a thinner balance through.
Reconciling
For per-request accounting, store usage.cost.total_cost with the X-Request-Id from the
same response. That pair is enough to reconcile your records against your ModelBeat usage
report line by line.
# with_raw_response, because the request id is a HEADER. It is not in the body.
raw = client.chat.completions.with_raw_response.create(
model="auto", messages=[...], max_tokens=200
)
r = raw.parse()
# Fields the OpenAI client does not know about arrive as plain dicts, so these are
# subscripted rather than dotted. `r.usage.cost.total_cost` raises AttributeError.
if r.usage and r.usage.cost:
ledger.record(
request_id=raw.headers.get("x-request-id"),
model=r.extra_fields["routing_info"]["model"],
usd=r.usage.cost["total_cost"],
)Aggregate spend, per-key budgets, and usage history live in the console.