Skip to content

Cost and budgets

Price a run, and know exactly when the number can be trusted.

The price table and full signatures are in the cost reference.

Unknown model price is undefined. Unknown cost is 0. The library never guesses.

A fabricated price is worse than an absent one when a budget gate depends on it. So there is no fallback estimate, no “average model” default, and no interpolation.

That rule is correct. Its consequence is the thing that has actually cost people money.

There are two very different situations, and they look identical from inside a comparison:

Situation pricingFor returns Cost Meaning
A cached replay a price 0 Costs zero. Genuinely free.
A local model undefined 0 Cannot be priced. No per-token cost exists.
claude-cli a price, but no usage 0 Cannot be measured. The provider reports no tokens.
An unknown hosted model undefined 0 Cannot be priced. Your gate is inert.

Only the first row is a real zero. Check pricingFor(...) === undefined before trusting a gate.

examples/cost-budget.mjs
// Pricing a run, and the difference between "costs zero" and "cannot be priced".
// Runs with no API key: MockProvider stands in for a real provider.
import {
MockProvider,
costOfRuns,
mockVerdict,
pricingFor,
runEnsemble,
} from "@hawkeyexl/inference";
const system = "You evaluate whether a page satisfies an assertion.";
const user = "# Assertion\nThe page documents authentication.\n\n# Page\nUse a bearer token.";
const verdicts = [mockVerdict("pass", 0.95), mockVerdict("pass", 0.93), mockVerdict("pass", 0.97)];
// A model the built-in table knows.
const priced = new MockProvider(verdicts, "claude-sonnet-4-5");
const pricedRuns = await runEnsemble({ provider: priced, system, user, runs: 3 });
const pricedPricing = pricingFor(priced.modelName());
console.log("known model:", priced.modelName());
console.log(" pricing:", pricedPricing);
console.log(" cost usd:", costOfRuns(pricedRuns, pricedPricing).toFixed(6));
// A pinned variant resolves by longest matching prefix, so it prices correctly.
console.log("pinned variant:", pricingFor("claude-sonnet-4-5-20250929"));
// A model the table does not know. Price is undefined — never a guess.
const unpriced = new MockProvider(verdicts, "some-new-model-v1");
const unpricedRuns = await runEnsemble({ provider: unpriced, system, user, runs: 3 });
const unpricedPricing = pricingFor(unpriced.modelName());
console.log("unknown model:", unpriced.modelName());
console.log(" pricing:", unpricedPricing);
console.log(" cost usd:", costOfRuns(unpricedRuns, unpricedPricing).toFixed(6));
// This is the trap. A budget gate over an unpriced model is not satisfied — it is inert.
const maxCostUsd = 0.5;
const spend = costOfRuns(unpricedRuns, unpricedPricing);
console.log(" budget gate passes:", spend < maxCostUsd, "— but only because the price is unknown");
console.log(" gate is inert:", unpricedPricing === undefined);
// Supply a price you know to make the gate real again.
const override = { inputPerMTok: 2, outputPerMTok: 8 };
const overridden = pricingFor(unpriced.modelName(), override);
console.log(" with override:", costOfRuns(unpricedRuns, overridden).toFixed(6), "usd");
known model: claude-sonnet-4-5
pricing: { inputPerMTok: 3, outputPerMTok: 15 }
cost usd: 0.009000
pinned variant: { inputPerMTok: 3, outputPerMTok: 15 }
unknown model: some-new-model-v1
pricing: undefined
cost usd: 0.000000
budget gate passes: true — but only because the price is unknown
gate is inert: true
with override: 0.005400 usd

pricingFor(model, override?) tries, in order:

  1. The explicit override, if you passed one.
  2. An exact match in PRICE_TABLE.
  3. The longest matching prefix.

Longest, not first — so a pinned variant like claude-sonnet-4-5-20250929 resolves to its family price, and adding a longer, more specific entry later can never be shadowed by a shorter one.

If none match, the result is undefined.

Two equivalent routes, depending on whether you have a spec or a model name to hand:

// On the spec — one object serves both construction and pricing.
const spec = { provider: "openai", model: "my-model", pricing: { inputPerMTok: 2, outputPerMTok: 8 } };
// Or directly, as the override argument.
const pricing = pricingFor(model, config.pricing);

If your users can configure a model, let them configure its price too. Otherwise every model you have not enumerated silently disables their budget.

  • Cached runs. costOfRuns skips anything flagged cached.
  • Runs with no usage. claude-cli reports none at all.
  • Local models. No table entry, by design.
  • The failed attempt of a retried request.

Check the accumulated cost before dispatching the next ensemble. Checking afterwards means the overspend has already happened.

let spent = 0;
const pricing = pricingFor(model, config.pricing);
for (const subject of subjects) {
if (pricing !== undefined && spent >= config.maxCostUsd) {
warn(`budget of $${config.maxCostUsd} reached; ${remaining} subjects skipped`);
break;
}
const runs = await runEnsemble({ provider, system, user: subject.body, runs: 3 });
spent += costOfRuns(runs, pricing);
}

The pricing !== undefined guard is what stops an unpriced model from turning the loop into an unbounded one. Without it, the gate is inert — see above.

  • Caching — the cheapest call is the one you do not make
  • Cost reference — the full price table and signatures