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.
The rule
Section titled “The rule”Unknown model price is
undefined. Unknown cost is0. 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.
The trap
Section titled “The trap”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.
Price a run
Section titled “Price a run”// 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.009000pinned 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 usdHow a price is resolved
Section titled “How a price is resolved”pricingFor(model, override?) tries, in order:
- The explicit override, if you passed one.
- An exact match in
PRICE_TABLE. - 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.
Supply a price you know
Section titled “Supply a price you know”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.
What is never charged
Section titled “What is never charged”- Cached runs.
costOfRunsskips anything flaggedcached. - Runs with no
usage.claude-clireports none at all. - Local models. No table entry, by design.
- The failed attempt of a retried request.
Gate before, not after
Section titled “Gate before, not after”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