# `PhoenixKitAI.Recommendations`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/v0.26.1/lib/phoenix_kit_ai/recommendations.ex#L1)

What to choose for an AI task, from live data only — there is no list of
"best models" in the module. What a model can do comes first, its price
second.

  * `ranked/2` — the models of the catalogues the endpoints use
    (`PhoenixKitAI.Capabilities.catalogue/1`) in groups: `verified` (the
    catalogue, or the endpoint's declaration, says the model has every
    need), `possible` (it lacks none, but at least one nobody verified —
    each row names those in `unverified`) and `experimental` (models of
    either kind that look experimental: an id with `preview`, `beta` or
    `exp`, a `:free` variant or a free price, an alias, or no price in a
    catalogue that prices its models), each ranked by the estimated price
    of one call.
  * The needs are the task's, with its images / documents / image
    generation needs replaced by the `filters` (`filterable/0`; the
    task's own by default). `refilter/2` regroups for other filters.
  * `cheapest/2` — the models fit for **all** of the task's hard needs,
    ranked by price, whatever they look like.
  * `worked/2` — the models that already worked in this task, from the
    usage journal over the last 30 days.
  * `for_task/2` — all of it, for the recommendations block.

## Ranking

The price of one call is `input_tokens × prompt_price + output_tokens ×
completion_price`, with the catalogue's dollars-per-token prices and the
task's typical call size: `typical_tokens: {in, out}` from its
declaration (`estimate: :declared`); else the median input and output
tokens of its successful calls in the journal over 30 days
(`estimate: :journal`); else nothing is known (`estimate: :rough`) and the
rows are ranked by the input price alone, with no price per call.

Inside each group the rows are ranked so. A model is in `verified` when
`PhoenixKitAI.Tasks.assess/2` says `:fit`, in `possible` when it says
`{:unknown, _}`, nowhere when it says `{:unfit, _}`; it must answer in
text unless image generation is among the needs. Retired models are left
out; deprecated ones are marked. The `/models` of
Anthropic, DeepSeek and Mistral list no prices, so their models are
unpriced (they are never mapped to an OpenRouter model by name), and no
provider but OpenRouter reports what a call cost; they come after the
priced ones, with one place kept for them among the rows.

A direct operator's plain list of ids (OpenAI, xAI, Z.ai — a `:plain`
source, `Capabilities.source_kind/2`) says nothing about any model: its
models are `possible` at best, never `verified`, marked
`:capabilities_not_reported`, and come after every model something is
known about. A capability filter does not hide them: «?» is not «no».

A model the catalogue gives no price is priced by the manual price an
endpoint there carries for it (`PhoenixKitAI.ManualPrice`), marked
`:manual_price`; so is a model that worked with costs counted from one.

## Journal

`worked/2` leaves out the rows the guard refused before any HTTP
(`metadata.refused`), so they count neither as calls nor as errors.

# `estimate`

```elixir
@type estimate() :: :declared | :journal | :rough
```

# `note`

```elixir
@type note() ::
  :alias
  | :cost_not_reported
  | :json_schema_openrouter_only
  | :free
  | :capabilities_not_reported
  | :manual_price
  | :table_price
```

# `alias?`

```elixir
@spec alias?(String.t()) :: boolean()
```

Whether the model id is an alias that can move to another model.

# `cheapest`

```elixir
@spec cheapest(String.t() | PhoenixKitAI.TaskSpec.t(), keyword()) :: %{
  rows: [map()],
  estimate: estimate(),
  tokens: {non_neg_integer(), non_neg_integer()} | nil
}
```

The cheapest fit models, `limit` (5) rows at most, each
`%{model, name, provider, base_url, integration_uuid, context_length,
capabilities, source_endpoint_uuid, price_per_call, price_known?, input_price_per_million,
deprecated?, notes}`. `price_per_call` is dollars, `nil` without a price
or an estimate of the call; `input_price_per_million` is dollars.

Options: `:limit`, `:days`, and `:catalogue` — the sources to rank instead
of the cache's (`%{base_url, kind, integration_uuid, models: [{id, caps}]}`).

# `filterable`

```elixir
@spec filterable() :: [PhoenixKitAI.TaskSpec.need()]
```

The capabilities the recommendations can be filtered by: images, documents, image generation.

# `for_task`

```elixir
@spec for_task(String.t() | PhoenixKitAI.TaskSpec.t(), keyword()) :: map()
```

Everything for a task (a key or a `%TaskSpec{}`): the groups of
`ranked/2` with its `filters`, `estimate` and `tokens`, and `worked`
(`worked/2`). It also keeps `spec` for `refilter/2` — but not the
catalogue: a panel keeps the result in its state.

# `for_tasks`

```elixir
@spec for_tasks([String.t() | PhoenixKitAI.TaskSpec.t()], keyword()) :: %{
  required(String.t()) =&gt; map()
}
```

`for_task/2` for several tasks at once, as a map by task key: the
catalogues are read once and the journal is asked twice in all (the
median call sizes and the models that worked, each grouped by task), not
twice per task — for a page that lists every task.

# `ranked`

```elixir
@spec ranked(String.t() | PhoenixKitAI.TaskSpec.t(), keyword()) :: map()
```

The models for the task in groups, `limit` (5) rows each:
`%{verified, possible, experimental, filters, estimate, tokens}`. A row is
as in `cheapest/2`, plus `unverified` — the needs nobody verified (`[]`
in `verified`).

Options: `:filters` (a subset of `filterable/0`; the task's own needs
among them by default), `:limit`, `:days`, `:catalogue` (as in
`cheapest/2`).

# `refilter`

```elixir
@spec refilter(map(), [atom()], keyword()) :: map()
```

The recommendations of `for_task/2` regrouped for other `filters`, from
the catalogues as the cache holds them now (or `:catalogue`, as in
`cheapest/2`); the journal part and the estimate are kept, nothing is
asked of the journal.

# `sources`

```elixir
@spec sources() :: [map()]
```

The catalogues the endpoints use, as the sources `cheapest/2` ranks: one
per base URL with a catalogue or a plain list (`kind: :plain`),
`%{base_url, kind, provider, endpoint_uuid, integration_uuid, models}` —
`provider` the endpoints' provider key, which names a plain list's
operator. Read once and passed as `:catalogue` when several tasks are
listed together. The integration is the one of an endpoint there (enabled
ones first), which the create button hands to the endpoint form.

# `worked`

```elixir
@spec worked(String.t() | PhoenixKitAI.TaskSpec.t(), keyword()) :: [map()]
```

The models that worked in the task over the last 30 days (`:days`), most
calls first, ten at most, one row per model and operator: `%{model,
provider, calls, avg_cost, error_share, avg_latency_ms, cost_reported?,
notes}`. `provider` is the endpoint's provider key, `nil` when the
endpoint is gone. `avg_cost` is US dollars, a `Decimal`
(`Request.format_cost/1` shows it) and `nil` when no call reported a
cost; `error_share` is 0.0–1.0. Refusals and cache hits are not calls.

---

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