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

A model reference returned by an optional model-catalog integration
Layer A serializable data.

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 the serializer's hydrator); the module
is registered with the serializer's known-modules list 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 `ALLM.Engine`'s `: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`.

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