Behaviour for computing the cost of an LLM call.
Hardcoded prices go stale fast, so Baton does not ship authoritative prices. Instead it defines this behaviour and calls whichever module the host configures:
config :baton, pricing: MyApp.LLMPricingYour implementation receives a usage map and returns a cost as Decimal.t(),
or nil if the model is unknown (cost is then recorded as nil rather than a
wrong number).
defmodule MyApp.LLMPricing do
@behaviour Baton.Pricing
@impl true
def cost(%{model: "claude-sonnet-4-" <> _, input_tokens: i, output_tokens: o}) do
Decimal.add(
Decimal.mult(Decimal.new(i), Decimal.from_float(3.0 / 1_000_000)),
Decimal.mult(Decimal.new(o), Decimal.from_float(15.0 / 1_000_000))
)
end
def cost(_usage), do: nil
endBaton.Pricing.Default is provided as a starting point and for tests, but
you should supply your own and keep it current.
Service tiers
A step running in batch mode (use Baton.LLMStep, mode: :batch) spends
roughly half as much per token, and its usage map says so with
service_tier: "batch". Nothing downstream of here can recover that — by the
time a cost is stored, a batch call and a live one look identical — so a
pricing module that cares about the discount must branch on it:
def cost(%{service_tier: "batch"} = usage), do: discounted(usage)
def cost(usage), do: standard(usage)Live calls carry no :service_tier key at all.
Summary
Callbacks
Return the cost in USD for a usage map, or nil if it can't be priced.
Functions
Compute cost via the configured pricing module.
Types
@type usage() :: %{ :model => String.t() | nil, optional(:service_tier) => String.t(), optional(:input_tokens) => non_neg_integer(), optional(:output_tokens) => non_neg_integer(), optional(:cache_read_tokens) => non_neg_integer(), optional(:cache_write_tokens) => non_neg_integer() }