Current section

Files

Jump to
llm_db lib llm_db pricing.ex
Raw

lib/llm_db/pricing.ex

defmodule LLMDB.Pricing do
@moduledoc """
Pricing pipeline for converting legacy cost data and applying provider defaults.
This module handles two key transformations during snapshot loading:
1. **Legacy cost conversion** - Converts the simple `cost` map (input/output/cache rates)
into the flexible `pricing.components` format for backward compatibility.
2. **Provider defaults** - Merges provider-level pricing defaults (e.g., tool pricing)
into each model's pricing, respecting merge strategies.
## Pipeline
The pricing transformations run during `LLMDB.Loader.load/1`:
models
|> Pricing.apply_cost_components() # Convert cost -> pricing.components
|> Pricing.apply_provider_defaults() # Merge provider defaults
## Pricing Structure
The `pricing` field on models contains:
%{
currency: "USD",
merge: "merge_by_id", # or "replace"
components: [
%{id: "token.input", kind: "token", unit: "token", per: 1_000_000, rate: 3.0},
%{id: "tool.web_search", kind: "tool", tool: "web_search", unit: "call", per: 1000, rate: 10.0}
]
}
See the [Pricing and Billing guide](pricing-and-billing.md) for full documentation.
"""
alias LLMDB.Merge
@doc """
Converts legacy `cost` fields to `pricing.components` format.
For each model with a `cost` map, generates corresponding pricing components:
| Cost Field | Component ID |
|------------|--------------|
| `input` | `token.input` |
| `output` | `token.output` |
| `cache_read` | `token.cache_read` |
| `cache_write` | `token.cache_write` |
| `reasoning` | `token.reasoning` |
Existing `pricing.components` are preserved and take precedence over
generated components (merged by ID).
## Examples
iex> models = [%{id: "gpt-4", provider: :openai, cost: %{input: 3.0, output: 15.0}}]
iex> [model] = LLMDB.Pricing.apply_cost_components(models)
iex> model.pricing.components
[
%{id: "token.input", kind: "token", unit: "token", per: 1_000_000, rate: 3.0},
%{id: "token.output", kind: "token", unit: "token", per: 1_000_000, rate: 15.0}
]
"""
@spec apply_cost_components([LLMDB.Model.t()]) :: [LLMDB.Model.t()]
def apply_cost_components(models) when is_list(models) do
Enum.map(models, &apply_cost_components_to_model/1)
end
@doc """
Applies provider-level pricing defaults to models.
For each model, looks up its provider's `pricing_defaults` and merges them
into the model's `pricing` field. The merge behavior depends on the model's
`pricing.merge` setting:
- `"merge_by_id"` (default) - Provider defaults are merged with model components
by ID. Model components override matching defaults.
- `"replace"` - Model pricing completely replaces provider defaults.
Models without existing `pricing` inherit the full provider defaults.
## Examples
iex> providers = [%{id: :openai, pricing_defaults: %{
...> currency: "USD",
...> components: [%{id: "tool.web_search", kind: "tool", rate: 10.0}]
...> }}]
iex> models = [%{id: "gpt-4", provider: :openai, pricing: nil}]
iex> [model] = LLMDB.Pricing.apply_provider_defaults(providers, models)
iex> model.pricing.components
[%{id: "tool.web_search", kind: "tool", rate: 10.0}]
"""
@spec apply_provider_defaults([LLMDB.Provider.t()], [LLMDB.Model.t()]) :: [LLMDB.Model.t()]
def apply_provider_defaults(providers, models) when is_list(providers) and is_list(models) do
defaults_by_provider =
Map.new(providers, fn provider ->
{provider.id, Map.get(provider, :pricing_defaults)}
end)
Enum.map(models, fn model ->
case Map.get(defaults_by_provider, model.provider) do
nil -> model
defaults -> apply_defaults_to_model(model, defaults)
end
end)
end
defp apply_defaults_to_model(model, defaults) do
case Map.get(model, :pricing) do
nil -> Map.put(model, :pricing, defaults)
pricing -> Map.put(model, :pricing, merge_pricing(defaults, pricing))
end
end
defp apply_cost_components_to_model(model) do
cost = Map.get(model, :cost) || Map.get(model, "cost")
if is_map(cost) and map_size(cost) > 0 do
pricing = Map.get(model, :pricing) || Map.get(model, "pricing") || %{}
existing_components = components_list(pricing)
cost_components = cost_components(cost)
merged_components = Merge.merge_list_by_id(cost_components, existing_components)
currency =
Map.get(pricing, :currency) || Map.get(pricing, "currency") || "USD"
updated_pricing =
pricing
|> Map.put(:currency, currency)
|> Map.put(:components, merged_components)
Map.put(model, :pricing, updated_pricing)
else
model
end
end
defp merge_pricing(defaults, pricing) do
case merge_mode(pricing) do
"replace" -> pricing
_ -> merge_by_id(defaults, pricing)
end
end
defp merge_mode(pricing) do
mode = Map.get(pricing, :merge) || Map.get(pricing, "merge")
case mode do
:replace -> "replace"
"replace" -> "replace"
:merge_by_id -> "merge_by_id"
"merge_by_id" -> "merge_by_id"
_ -> "merge_by_id"
end
end
defp merge_by_id(defaults, pricing) do
currency =
Map.get(pricing, :currency) ||
Map.get(pricing, "currency") ||
Map.get(defaults, :currency) ||
Map.get(defaults, "currency")
default_components = components_list(defaults)
pricing_components = components_list(pricing)
merged_components = Merge.merge_list_by_id(default_components, pricing_components)
pricing
|> Map.put(:currency, currency)
|> Map.put(:components, merged_components)
end
defp components_list(pricing) do
Map.get(pricing, :components) || Map.get(pricing, "components") || []
end
defp cost_components(cost) when is_map(cost) do
[]
|> maybe_add_token_component("token.input", Map.get(cost, :input) || Map.get(cost, "input"))
|> maybe_add_token_component(
"token.output",
Map.get(cost, :output) || Map.get(cost, "output")
)
|> maybe_add_token_component(
"token.cache_read",
Map.get(cost, :cache_read) || Map.get(cost, "cache_read") ||
Map.get(cost, :cached_input) || Map.get(cost, "cached_input")
)
|> maybe_add_token_component(
"token.cache_write",
Map.get(cost, :cache_write) || Map.get(cost, "cache_write")
)
|> maybe_add_token_component(
"token.reasoning",
Map.get(cost, :reasoning) || Map.get(cost, "reasoning")
)
end
defp maybe_add_token_component(components, _id, nil), do: components
defp maybe_add_token_component(components, id, rate) when is_number(rate) do
components ++ [%{id: id, kind: "token", unit: "token", per: 1_000_000, rate: rate}]
end
defp maybe_add_token_component(components, _id, _rate), do: components
end