Show 'this call will cost about $X' before sending — and be right
The estimate endpoint answers in milliseconds but has two failure modes that return a confident wrong number. How to build a price preview you can defend.
Send the real messages array and an expected_output_tokens guess to Infrai’s estimate endpoint, cap the reply with max_tokens so the estimate becomes a ceiling rather than a guess, and reconcile against the actual cost that comes back on the response. That’s the accurate version. The inaccurate version — passing token counts instead of messages — returns ok: true and a number that is quietly wrong.
Two traps do most of the damage here, and both are silent. Neither raises an error, which is exactly why an internal price preview drifts from the invoice and nobody notices for a month.
The call that works
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST "https://api.infrai.cc/v1/ai/cost/estimate" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": "Summarise this 400-word support ticket into one sentence and pick a tag."}
],
"expected_output_tokens": 120
}'
{
"ok": true,
"data": {
"model": "openai/gpt-4o-mini",
"vendor": "openai",
"vendor_region": "western",
"prompt_tokens": 35,
"expected_output_tokens": 120,
"breakdown": {
"currency": "USD",
"input_cost": 5.25e-06,
"output_cost": 7.2e-05,
"markup": 3.86e-06,
"cache_discount": 0.0,
"batch_discount": 0.0,
"final": 8.111e-05
}
}
}
The endpoint tokenised the messages itself — that’s the prompt_tokens: 35 — and the split between input_cost and output_cost tells you which half of the estimate is the guess. Input is measured. Output is your assumption, and on this call it’s 89% of the total.
Estimates are free and don’t touch the trial credit, so calling one on every keystroke of a prompt editor is fine.
Trap one: token counts are accepted and ignored
This is the one that ships to production.
curl -sS -X POST "https://api.infrai.cc/v1/ai/cost/estimate" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model": "openai/gpt-4o-mini", "prompt_tokens": 50000, "completion_tokens": 1000}'
{
"ok": true,
"data": {
"model": "openai/gpt-4o-mini",
"prompt_tokens": 0,
"expected_output_tokens": 500,
"breakdown": { "input_cost": 0.0, "output_cost": 0.0003, "markup": 1.5e-05, "final": 0.000315 }
}
}
Fifty thousand prompt tokens went in. Zero came out the other side, and the output guess silently defaulted to 500. A team that counted tokens locally with tiktoken and passed the numbers along would show every user the same fixed price regardless of prompt size — and the discovery manifest publishes no parameter list for this route, so nothing tells you the fields were dropped. Echo prompt_tokens back from the response into your UI and the bug becomes visible immediately.
Trap two: the estimator knows a different model list
There are effectively two model tables behind the API, and this is a real limitation rather than a quirk.
curl -sS -X POST "https://api.infrai.cc/v1/ai/cost/estimate" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-5-mini", "messages": [{"role": "user", "content": "ping"}], "expected_output_tokens": 50}'
{
"ok": false,
"error": {
"code": "MODEL_NOT_FOUND",
"http_status": 400,
"message": "model string not recognized.",
"code_detail": "gpt-5-mini",
"retryable": false
}
}
gpt-5-mini is a live, servable model — it’s in the catalogue and it answers chat requests. The estimator still rejects it. Verified 2026-07-26, only six identifiers resolved: openai/gpt-4o, its bare form gpt-4o, openai/gpt-4o-mini, deepseek/deepseek-chat, deepseek/deepseek-reasoner and openai/text-embedding-3-small. Even auto fails, so smart routing has no price preview.
| Source | What it knows | Latency | Cost |
|---|---|---|---|
POST /v1/ai/cost/estimate | six model ids, full markup breakdown | ~8 ms | free |
POST /v1/ai/cost/compare | the same six, ranked | ~4 ms | free |
POST /v1/ai/tokens/count | real prompt tokens for any served model | ~5 ms | free |
GET /v1/ai/models | per-Mtok input and output rates for every served model | one call, cacheable | free |
response infrai.cost_usd | what you were actually charged | after the fact | — |
The estimator you can actually ship
For any model outside those six, build the quote from the two free primitives: real token counts and the live rate card. It’s about thirty lines.
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const BASE = "https://api.infrai.cc";
const auth = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
async function rateCard() {
const res = await fetch(`${BASE}/v1/ai/models?capability=chat&available=true`, { method: "GET", headers: auth });
if (!res.ok) throw new Error(`catalogue lookup failed: ${res.status}`);
const { data } = await res.json();
return new Map(data.map((m) => [m.id, m]));
}
async function promptTokens(model, messages) {
const res = await fetch(`${BASE}/v1/ai/tokens/count`, {
method: "POST",
headers: auth,
body: JSON.stringify({ model, messages }),
});
if (!res.ok) throw new Error(`token count failed: ${res.status}`);
const { data } = await res.json();
return data.prompt_tokens;
}
export async function quote({ model, messages, maxTokens }) {
const card = await rateCard();
const rates = card.get(model);
if (!rates) throw new Error(`${model} is not currently servable`);
const inTok = await promptTokens(model, messages);
const ceiling = (inTok * rates.price_input_per_mtok + maxTokens * rates.price_output_per_mtok) / 1e6;
return { model, prompt_tokens: inTok, max_output_tokens: maxTokens, ceiling_usd: ceiling };
}
const q = await quote({
model: "gpt-5-mini",
messages: [{ role: "user", content: "Draft a refund email for invoice 5512." }],
maxTokens: 400,
});
console.log(`at most $${q.ceiling_usd.toFixed(6)} (${q.prompt_tokens} prompt tokens)`);
Because maxTokens is the same value you’ll pass as max_tokens on the real call, the number you show is a bound the API can’t exceed — say “at most $0.0008” rather than “about $0.0008”, and you’ll never owe anyone an explanation. On a Node 22 runtime the whole quote costs two free round trips, and caching the rate card for a minute cuts it to one.
Ranking options in front of the user
When the choice itself is the question, compare rather than estimate one at a time.
curl -sS -X POST "https://api.infrai.cc/v1/ai/cost/compare" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"models": ["openai/gpt-4o", "openai/gpt-4o-mini", "deepseek/deepseek-chat"],
"messages": [{"role": "user", "content": "Classify this ticket."}],
"expected_output_tokens": 120
}'
It comes back sorted, cheapest first: for that 13-token prompt, deepseek/deepseek-chat at $0.0000354, openai/gpt-4o-mini at $0.0000777, openai/gpt-4o at $0.00196. A 55× spread on identical input is the argument for making the model a config value — switching is a string change here, not a re-integration.
Reconcile, or the preview rots
Every chat response carries what you were really charged, in an infrai object and in X-Infrai-Cost-Usd. Log the estimate and the actual side by side, and the ratio tells you whether your output guess is calibrated.
curl -sS -D - -o /dev/null -X POST "https://api.infrai.cc/v1/chat/completions" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"model": "glm-4-flash", "max_tokens": 10, "messages": [{"role": "user", "content": "Reply with the single word OK."}]}' \
| grep -i "x-infrai"
For the account-level truth, GET /v1/account/usage breaks 30 days of spend down by capability, and GET /v1/account/balance returns the remaining credit with a runway_days figure. That’s the reconciliation loop closed with two more free reads.
What the estimate can’t tell you
It doesn’t model cache hits — cache_discount came back 0.0 on every call we made — so a repeated prompt may cost less than quoted. It doesn’t know about failover, so if the primary vendor is down and the call lands elsewhere, the actual rate differs. And it ignores everything outside inference: if your request also stores an artefact or sends an email, those are separate line items on the same bill.
OpenRouter shows a comparable per-request cost in its response, and OpenAI’s own pricing page plus a local tiktoken count is a perfectly good estimator if you only ever call OpenAI models. The reason to do it here is that the same key, the same usage view and the same balance cover the inference, the storage, the queue and the notification — so “what will this cost” has one answer instead of four.