LLMDB.Pricing (LLM DB v2026.9.8)

Copy Markdown View Source

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 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

component_role()

@type component_role() :: :rate | :derived_rate | :modifier

validation_error()

@type validation_error() :: %{
  :code => atom(),
  :message => String.t(),
  optional(:component_id) => String.t() | nil,
  optional(atom()) => term()
}

Functions

apply_cost_components(models)

@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 FieldComponent ID
inputtoken.input
outputtoken.output
cache_readtoken.cache_read
cache_writetoken.cache_write
reasoningtoken.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}
]

apply_provider_defaults(providers, models)

@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}]

component_role(component)

@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.

components_for(model, context \\ %{})

@spec components_for(map(), map() | keyword()) :: %{
  components: [map()],
  unresolved: [map()]
}

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"]

select_components(model, 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.

validate_components(components)

@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.