# `ALLM.ModelRef`
[🔗](https://github.com/cykod/ALLM/blob/v0.3.0/lib/allm/model_ref.ex#L1)

Layer A — model reference returned by the optional `LLMDB` catalog.

See spec §6.3 (lines 637-648). Carries the catalog's view of a single model:
provider, id, capability flags (tools, json_native, vision), context/output
limits, per-million-token pricing, and an opaque `:metadata` bag.

A `%ModelRef{}` is plain serializable data — no PIDs, refs, funs, or API
keys. It round-trips through `:erlang.term_to_binary/1` and JSON
(`ALLM.Serializer.to_json!/1`) just like every other Layer A struct.

Construction is via `new/1`. Hydration from a JSON-decoded map is via
`__from_tagged__/1` (called by `ALLM.Serializer.hydrate/1`); the module is
registered in `ALLM.Serializer.@known_modules` so tagged JSON
(`%{"__type__" => "ALLM.ModelRef", "data" => ...}`) decodes automatically.

## Layer A nested-map JSON asymmetry (carve-out)

`:capabilities`, `:limits`, `:pricing`, and `:metadata` are opaque map
bags whose **nested keys are not atomized on JSON rehydration**. ETF
round-trip is byte-identical, but `Jason.encode!/1` →
`Serializer.from_json/1` produces:

    iex> ref = ALLM.ModelRef.new(
    ...>   provider: :openai, id: "x",
    ...>   capabilities: %{tools: %{enabled: false}}
    ...> )
    iex> {:ok, hydrated} = ALLM.Serializer.from_json(ALLM.Serializer.to_json!(ref))
    iex> hydrated.capabilities
    %{"tools" => %{"enabled" => false}}

This matches the Phase 1 `Engine.metadata` precedent — opaque
caller-owned maps don't get atom-key restoration because the closed-set
of valid keys can't be bounded at hydration time. **Consumers handle
this asymmetry directly:** `ALLM.Capability.preflight/2` and
`ALLM.Capability.populate_costs/2` pattern-match on both atom-keyed
and string-keyed shapes, so a JSON-rehydrated `%ModelRef{}` pre-flights
and prices identically to an in-process one. A naive consumer that
pattern-matches only on atom-keyed shapes will silently bypass the
rehydrated ref — see `ALLM.Capability` `@moduledoc`.

## Examples

    iex> ref = ALLM.ModelRef.new(provider: :openai, id: "gpt-4.1-mini")
    iex> ref.provider
    :openai
    iex> ref.id
    "gpt-4.1-mini"
    iex> ref.metadata
    %{}

# `capabilities`

```elixir
@type capabilities() :: %{
  optional(:tools) =&gt; %{enabled: boolean()},
  optional(:json_native) =&gt; boolean(),
  optional(atom()) =&gt; term()
}
```

# `limits`

```elixir
@type limits() :: %{
  optional(:context) =&gt; pos_integer(),
  optional(:output) =&gt; pos_integer(),
  optional(atom()) =&gt; term()
}
```

# `pricing`

```elixir
@type pricing() :: %{input: number(), output: number()} | nil
```

# `t`

```elixir
@type t() :: %ALLM.ModelRef{
  capabilities: capabilities() | nil,
  id: String.t() | nil,
  limits: limits() | nil,
  metadata: map(),
  pricing: pricing(),
  provider: atom() | nil
}
```

# `new`

```elixir
@spec new(keyword()) :: t()
```

Build a `%ALLM.ModelRef{}` from keyword opts.

Accepts any subset of the documented struct fields; unknown keys raise
`KeyError` via `struct!/2` (Phase 1 convention).

## Examples

    iex> ref = ALLM.ModelRef.new(
    ...>   provider: :openai,
    ...>   id: "gpt-4.1-mini",
    ...>   capabilities: %{tools: %{enabled: true}, json_native: true},
    ...>   pricing: %{input: 0.15, output: 0.6}
    ...> )
    iex> ref.pricing
    %{input: 0.15, output: 0.6}

---

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