LLM Cost Estimate

Estimates an LLM batch's tokens and cost from your own dated price table before it runs, so a missing max_tokens or wrong model doesn't cause a surprise bill.

StopsSurprise bill from an LLM batch

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/llm-cost-estimate.json

The files land in your project. They're yours to edit.

Usage

llm-cost-estimate-usage.tsx
import { LlmCostEstimate, type ModelPrice } from "@/components/ui/llm-cost-estimate"

<LlmCostEstimate
  models={MODELS}
  pricesAsOf="2026-10-01"
  now={new Date(2026, 9, 14, 11).getTime()}
  items={50_000}
  inputTokens={1_240}
  expectedOutputTokens={60}
  lastRunCost={3.1}
  locale="en-US"
  onRun={(run) => new Promise<void>((resolve) => setTimeout(() => { setRan(`Started on ${run.model}${run.batch ? " as a batch job" : ""}, max_tokens ${run.maxTokens ?? "not set"}`); resolve() }, 600))}
/>

What it catches

  • No max_tokens: worst-case cost shown
  • Request bigger than the model's context window
  • Run many times larger than the last one
  • Premium model picked for a bulk job, with a switch
  • Batch API discount on bulk jobs
  • Price table older than staleAfterDays

Props

LlmCostEstimate props
PropTypeDefault
models* (required)

Your price table. Nothing is hard-coded: prices change, so pass the ones you read and when.

ModelPrice[]—
pricesAsOf* (required)

When the prices in `models` were read. Shown, and flagged once older than `staleAfterDays`.

string | number | Date—
model

Controlled model id.

string—
defaultModel

Initial model id when uncontrolled. Defaults to the first row.

string—
onModelChange

Called with the chosen model id.

(id: string) => void—
maxTokens

Controlled max_tokens; `null` means not set.

number | null—
defaultMaxTokens

Initial max_tokens when uncontrolled.

number | nullnull
onMaxTokensChange

Called with the new max_tokens, or null when cleared.

(value: number | null) => void—
batch

Controlled: send through the Batch API (needs `batchDiscount` on the model).

boolean—
defaultBatch

Initial batch choice when uncontrolled.

booleanfalse
onBatchChange

Called when the Batch API toggle changes.

(batch: boolean) => void—
items

Requests in the run.

number1
prompt

One request's prompt, to estimate input tokens from. Ignored when `inputTokens` is given.

string—
inputTokens

Exact input tokens per request from a real tokenizer.

number—
expectedOutputTokens

Typical reply length in tokens, if you know it.

number—
lastRunCost

Cost of the last comparable run, to catch a run many times bigger.

number—
jumpFactor

Multiple of the last run that counts as a jump.

number10
bulkItems

Requests at which the run counts as bulk, for the costly-model check.

number100
costlyFactor

How many times pricier than the cheapest fitting model counts as costly.

number10
staleAfterDays

Days after pricesAsOf before the price table is called stale.

number60
currency

ISO 4217 code for the table's prices.

string"USD"
locale

BCP 47 locale for numbers and currency.

string—
now

"Now" in epoch ms, for deterministic rendering. Read from the clock after mount when omitted.

number—
onRun

Called on Run. With findings left, the first click jumps to them and the second runs. May return a Promise.

(run: { model: string; maxTokens: number | null; batch: boolean; expectedCost: number; worstCost: number }) => void | Promise<void>—
messages

Any visible string.

Partial<LlmCostMessages>—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Model select and max-tokens input have visible labels
  • Inputs get aria-invalid when a finding applies
  • Focus moves to the first finding's fix after a change

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Set spend limits and max_tokens on the server or API key; the estimate doesn't cap anything.

  • Token counts are estimates unless you pass inputTokens; real tokenizers differ by model
  • Prices come only from your table; it doesn't fetch provider pricing
  • Doesn't enforce a budget; your API key limits or server must

What it stops

Surprise bill from an LLM batch

Also known as

token counter · openai cost · llm pricing · max_tokens · context window · batch api · prompt playground · anthropic api

Field type

Dependencies

Pairs well with