PhoenixKitAI.Recommendations (PhoenixKitAI v0.26.1)

Copy Markdown View Source

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.

Summary

Functions

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

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.

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

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

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

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.

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.

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.

Types

estimate()

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

note()

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

Functions

alias?(model)

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

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

cheapest(task, opts \\ [])

@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()

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

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

for_task(task, opts \\ [])

@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(tasks, opts \\ [])

@spec for_tasks([String.t() | PhoenixKitAI.TaskSpec.t()], keyword()) :: %{
  required(String.t()) => 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(task, opts \\ [])

@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(recs, filters, opts \\ [])

@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()

@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(task, opts \\ [])

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