lib/planck/ai/llmdb.ex

defmodule Planck.AI.LLMDB do
  @moduledoc false

  require Logger

  alias Planck.AI.Model

  @loaded_key {__MODULE__, :loaded}

  @doc """
  Returns all `Planck.AI.Model` structs for the given LLMDB provider atom.

  Lazily loads the LLMDB snapshot on first call. Returns `[]` on any failure.
  """
  @spec models(atom()) :: [Model.t()]
  def models(provider) do
    unless :persistent_term.get(@loaded_key, false) do
      case LLMDB.load() do
        {:ok, _} ->
          :persistent_term.put(@loaded_key, true)

        {:error, reason} ->
          Logger.warning("[Planck.AI] LLMDB failed to load: #{inspect(reason)}")
      end
    end

    provider
    |> LLMDB.models()
    |> Enum.map(&translate/1)
  end

  # Translates an LLMDB.Model into a Planck.AI.Model.
  defp translate(m) do
    %Model{
      id: m.id,
      name: m.name || m.id,
      provider: m.provider,
      context_window: context_window(m),
      max_tokens: max_tokens(m),
      supports_thinking: supports_thinking(m),
      input_types: input_types(m),
      cost: cost(m)
    }
  end

  defp context_window(%{limits: %{context: ctx}}) when is_integer(ctx) and ctx > 0, do: ctx
  defp context_window(_), do: 4_096

  defp max_tokens(%{limits: %{output: out}}) when is_integer(out) and out > 0, do: out
  defp max_tokens(_), do: 2_048

  defp supports_thinking(%{capabilities: %{reasoning: %{enabled: true}}}), do: true
  defp supports_thinking(_), do: false

  defp input_types(%{modalities: %{input: inputs}}) when is_list(inputs) do
    filtered = Enum.filter(inputs, &(&1 in [:text, :image]))
    if filtered == [], do: [:text], else: filtered
  end

  defp input_types(_), do: [:text]

  defp cost(%{cost: cost}) when is_map(cost) do
    %{
      input: Map.get(cost, :input) || 0.0,
      output: Map.get(cost, :output) || 0.0,
      cache_read: Map.get(cost, :cache_read) || 0.0,
      cache_write: Map.get(cost, :cache_write) || 0.0
    }
  end

  defp cost(_), do: %{input: 0.0, output: 0.0, cache_read: 0.0, cache_write: 0.0}
end