Pricing pipeline for converting legacy cost data and applying provider defaults.
This module handles two key transformations during snapshot loading:
Legacy cost conversion - Converts the simple
costmap (input/output/cache rates) into the flexiblepricing.componentsformat for backward compatibility.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 defaultsPricing 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 for full documentation.
Summary
Functions
Converts legacy cost fields to pricing.components format.
Applies provider-level pricing defaults to models.
Returns the declared or inferred role of a pricing component.
Selects pricing components that apply for a request context.
Selects components and applies strict structural and selection checks.
Validates component roles, conditions, IDs, and cross-component references.
Types
Functions
@spec apply_cost_components([LLMDB.Model.t()]) :: [LLMDB.Model.t()]
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). Model-level excluded_cost_components
suppresses specified legacy conversions without deleting explicit components
or changing the legacy cost summary. This avoids counting included reasoning
twice or interpreting subscription summaries as token-credit tariffs.
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_provider_defaults([LLMDB.Provider.t()], [LLMDB.Model.t()]) :: [ LLMDB.Model.t() ]
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 component_role(map()) :: {:ok, component_role()} | {:error, :missing_component_role | :ambiguous_component_role | :invalid_component_role}
Returns the declared or inferred role of a pricing component.
The optional role field is authoritative. Components without it retain the
legacy behavior: rate, derives_from, and applies_to identify direct
rates, derived rates, and modifiers. A component with none or more than one
of these signatures is invalid for strict selection.
Selects pricing components that apply for a request context.
This helper does not calculate final cost. It separates components with fully
satisfied conditions from components that cannot be resolved because the
supplied context is incomplete. Missing or nil context values are unknown;
a known non-matching application condition or matching exclusion rules a
component out even if another condition is unknown.
Conditions are conjunctive and accept atom or string keys, nested maps, and
numeric gt/gte/lt/lte comparisons. Selected components are returned
unchanged: callers must resolve derived rates and apply matching modifiers
once. This helper neither resolves overlapping rate components nor validates
provider request eligibility. See the pricing guide for current-model examples.
Examples
iex> model = %{pricing: %{components: [
...> %{id: "token.input", rate: 5.0},
...> %{id: "token.input.long_context", rate: 10.0, applies_when: %{input_tokens: %{gt: 272_000}}}
...> ]}}
iex> LLMDB.Pricing.components_for(model, input_tokens: 900_000).components |> Enum.map(& &1.id)
["token.input", "token.input.long_context"]
@spec select_components(map(), map() | keyword()) :: {:ok, %{components: [map()], unresolved: [map()], errors: []}} | {:error, %{components: [map()], unresolved: [map()], errors: [validation_error()]}}
Selects components and applies strict structural and selection checks.
components_for/2 remains the backward-compatible low-level selector.
select_components/2 returns {:ok, result} only when component validation
succeeds, no condition is unresolved, and no rate group selects more than one
rate. A group with rate_group_policy: "exactly_one" must select one rate.
The result always contains components, unresolved, and errors.
@spec validate_components([map()]) :: :ok | {:error, [validation_error()]}
Validates component roles, conditions, IDs, and cross-component references.
This validation is additive. Legacy components do not need a role; their
role is inferred with component_role/1. The function does not change the
components.