# `LLMDB.Pricing`
[🔗](https://github.com/agentjido/llmdb/blob/main/lib/llm_db/pricing.ex#L1)

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.

# `component_role`

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

# `validation_error`

```elixir
@type validation_error() :: %{
  :code =&gt; atom(),
  :message =&gt; String.t(),
  optional(:component_id) =&gt; String.t() | nil,
  optional(atom()) =&gt; term()
}
```

# `apply_cost_components`

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

# `apply_provider_defaults`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
