Packages
llm_db
2026.3.2
2026.7.3
2026.7.2
2026.7.1
2026.7.0
2026.6.4
2026.6.3
2026.6.2
2026.6.1
2026.6.0
2026.5.2
2026.5.1
2026.5.0
2026.4.8
2026.4.7
2026.4.6
2026.4.5
2026.4.4
2026.4.3
2026.4.2
2026.4.1
2026.4.0
2026.3.3
2026.3.2
2026.3.1
2026.3.0
2026.2.9
2026.2.8
2026.2.7
2026.2.6
2026.2.5
2026.2.4
2026.2.3
2026.2.2
2026.2.1
2026.2.0
2026.1.5
2026.1.4
2026.1.3
2026.1.2
2026.1.1
2026.1.0
2025.12.4
2025.12.3
2025.12.2
2025.12.1
2025.11.18-preview
2025.11.14-preview
2025.11.7-preview
LLM model metadata catalog with fast, capability-aware lookups.
Current section
Files
Jump to
Current section
Files
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