diff --git a/README.md b/README.md
index ba5d907..ad44d94 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ Bodhi supports multiple AI providers that can be switched via configuration.
#### OpenRouter (Default)
- **Module:** `Bodhi.OpenRouter`
-- **Default Model:** `deepseek/deepseek-r1-0528:free`
+- **Default Model:** `openrouter/free` (when no model is selected in admin)
- **Environment Variable:** `OPENROUTER_API_KEY`
- **Website:** https://openrouter.ai/
@@ -33,10 +33,10 @@ To switch AI providers, update `config/config.exs`:
```elixir
# Use OpenRouter (default)
-config :bodhi, :ai_client, Bodhi.OpenRouter
+config :bodhi, :llm_provider, Bodhi.OpenRouter
# Use Google Gemini
-config :bodhi, :ai_client, Bodhi.Gemini
+config :bodhi, :llm_provider, Bodhi.Gemini
```
### Setting Up API Keys
@@ -51,21 +51,13 @@ config :bodhi, :ai_client, Bodhi.Gemini
3. Reload environment: `direnv allow` (if using direnv)
-### Changing OpenRouter Model
+### Selecting an OpenRouter Model
-Edit `lib/bodhi/open_router.ex` and modify the `@default_model` attribute:
-
-```elixir
-@default_model "deepseek/deepseek-r1-0528:free" # Current default
-
-# Other popular models:
-# @default_model "anthropic/claude-3.5-sonnet"
-# @default_model "openai/gpt-4-turbo"
-# @default_model "meta-llama/llama-3.1-70b-instruct"
-# @default_model "google/gemini-pro-1.5"
-```
-
-See all available models at: https://openrouter.ai/models
+In the admin UI, open **LLM Configurations**, choose **Sync Models** to
+load available OpenRouter models, then edit a model to set optional
+temperature and max token values. Choose **Use this model** to select one
+model for the bot. Choose **Use default model** to clear the selection and
+return to `openrouter/free`.
## Features
diff --git a/lib/bodhi/llm_configs.ex b/lib/bodhi/llm_configs.ex
index 4e76843..5bda932 100644
--- a/lib/bodhi/llm_configs.ex
+++ b/lib/bodhi/llm_configs.ex
@@ -9,8 +9,8 @@ defmodule Bodhi.LlmConfigs do
import Ecto.Query, warn: false
alias Bodhi.Cache
- alias Bodhi.Repo
alias Bodhi.LlmConfigs.LlmConfig
+ alias Bodhi.Repo
@cache_key :active_llm_configs
@@ -45,7 +45,7 @@ defmodule Bodhi.LlmConfigs do
def get_llm_config!(id), do: Repo.get!(LlmConfig, id)
@doc """
- Returns all active configs ordered by position.
+ Returns the selected model as a list of zero or one records.
"""
@spec get_active_configs() :: [LlmConfig.t()]
def get_active_configs do
@@ -55,12 +55,62 @@ defmodule Bodhi.LlmConfigs do
|> Repo.all()
end
+ @doc "Selects a model and clears any previous selection."
+ @spec select_llm_config(non_neg_integer()) ::
+ {:ok, LlmConfig.t()} | {:error, Ecto.Changeset.t()}
+ def select_llm_config(id) do
+ result = Repo.transaction(fn -> select_config_in_transaction(id) end)
+
+ case result do
+ {:ok, config} ->
+ invalidate_cache()
+ {:ok, config}
+
+ {:error, changeset} ->
+ {:error, changeset}
+ end
+ end
+
+ defp select_config_in_transaction(id) do
+ Repo.update_all(LlmConfig, set: [active: false])
+
+ case Repo.get(LlmConfig, id) do
+ nil ->
+ Repo.rollback(
+ LlmConfig.changeset(%LlmConfig{}, %{})
+ |> Ecto.Changeset.add_error(:id, "model does not exist")
+ )
+
+ config ->
+ config
+ |> Ecto.Changeset.change(active: true)
+ |> Ecto.Changeset.unique_constraint(
+ :active,
+ name: :llm_configs_one_active_index
+ )
+ |> Repo.update()
+ |> rollback_on_error()
+ end
+ end
+
+ defp rollback_on_error({:ok, config}), do: config
+ defp rollback_on_error({:error, changeset}), do: Repo.rollback(changeset)
+
+ @doc "Clears the selected model so OpenRouter uses its default."
+ @spec clear_llm_config_selection() :: :ok
+ def clear_llm_config_selection do
+ Repo.update_all(LlmConfig, set: [active: false])
+ invalidate_cache()
+ end
+
@doc """
Creates an LLM config.
"""
@spec create_llm_config(map()) ::
{:ok, LlmConfig.t()} | {:error, Ecto.Changeset.t()}
def create_llm_config(attrs) do
+ attrs = Map.drop(attrs, [:active, "active"])
+
result =
%LlmConfig{}
|> LlmConfig.changeset(attrs)
@@ -76,13 +126,15 @@ defmodule Bodhi.LlmConfigs do
@spec update_llm_config(LlmConfig.t(), map()) ::
{:ok, LlmConfig.t()} | {:error, Ecto.Changeset.t()}
def update_llm_config(%LlmConfig{} = config, attrs) do
+ attrs = Map.drop(attrs, [:active, "active"])
+
result =
config
|> LlmConfig.changeset(attrs)
|> Repo.update()
with {:ok, _} <- result do
- if config.active || touches_active?(attrs) do
+ if config.active do
invalidate_cache()
end
end
@@ -251,16 +303,6 @@ defmodule Bodhi.LlmConfigs do
ArgumentError -> :position
end
- defp maybe_invalidate_cache({:ok, _}, attrs) do
- if touches_active?(attrs), do: invalidate_cache()
- end
-
- defp maybe_invalidate_cache(_, _), do: :ok
-
- defp touches_active?(attrs) do
- active =
- Map.get(attrs, :active) || Map.get(attrs, "active")
-
- active == true
- end
+ defp maybe_invalidate_cache({:ok, _}, _attrs), do: :ok
+ defp maybe_invalidate_cache({:error, _}, _attrs), do: :ok
end
diff --git a/lib/bodhi/llm_configs/llm_config.ex b/lib/bodhi/llm_configs/llm_config.ex
index 1313235..ac38712 100644
--- a/lib/bodhi/llm_configs/llm_config.ex
+++ b/lib/bodhi/llm_configs/llm_config.ex
@@ -2,18 +2,15 @@ defmodule Bodhi.LlmConfigs.LlmConfig do
@moduledoc """
Schema for LLM model configurations.
- Each config represents a model that can be used via
- OpenRouter. Multiple configs can be active simultaneously
- -- their `position` determines fallback order (lower =
- higher priority). Active configs are sent as OpenRouter's
- `models` array with `route: "fallback"`.
+ Each config represents an OpenRouter model. At most one
+ configuration can be active at a time.
"""
use Ecto.Schema
import Ecto.Changeset
@allowed_fields ~w(
- name model position temperature max_tokens active
+ name model position temperature max_tokens
)a
@required_fields ~w(name model position)a
diff --git a/lib/bodhi/open_router.ex b/lib/bodhi/open_router.ex
index c8bbe7f..15741ae 100644
--- a/lib/bodhi/open_router.ex
+++ b/lib/bodhi/open_router.ex
@@ -2,10 +2,8 @@ defmodule Bodhi.OpenRouter do
@moduledoc """
OpenRouter API wrapper.
- Provides unified access to multiple AI models through
- OpenAI-compatible API. Supports database-backed model
- configuration with multi-model fallback via OpenRouter's
- `models` array and `route: "fallback"`.
+ Provides access to a selected OpenRouter model through its
+ OpenAI-compatible API, with `openrouter/free` as the default.
"""
@behaviour Bodhi.Behaviours.LLMProvider
@@ -70,7 +68,7 @@ defmodule Bodhi.OpenRouter do
Returns active LLM configs from cache or database.
Results are cached for 5 minutes. Returns an empty list
- when no active configs exist (falls back to default model).
+ when no model is selected (falls back to the default model).
"""
@spec resolve_config() :: [LlmConfig.t()]
def resolve_config do
@@ -88,11 +86,8 @@ defmodule Bodhi.OpenRouter do
@doc """
Builds the JSON request body for OpenRouter.
- - Single active model: uses `model` key
- - Multiple active models: uses `models` array +
- `route: "fallback"`
- - No DB config: uses `@default_model`
- - Temperature/max_tokens from highest-priority config
+ Uses the selected model and its optional parameters, or the
+ default model when no configuration is selected.
"""
@spec build_body([map()], String.t(), [LlmConfig.t()]) ::
String.t()
@@ -140,17 +135,12 @@ defmodule Bodhi.OpenRouter do
Map.put(body, :model, @default_model)
end
- defp put_model_config(body, [single]) do
- Map.put(body, :model, single.model)
+ defp put_model_config(body, [selected | _]) do
+ Map.put(body, :model, selected.model)
end
- defp put_model_config(body, configs) do
- models = Enum.map(configs, & &1.model)
-
- body
- |> Map.put(:models, models)
- |> Map.put(:route, "fallback")
- end
+ defp put_model_config(body, _configs),
+ do: Map.put(body, :model, @default_model)
defp put_optional_params(body, []) do
body
diff --git a/lib/bodhi_web.ex b/lib/bodhi_web.ex
index 2ee9532..2c0dcc5 100644
--- a/lib/bodhi_web.ex
+++ b/lib/bodhi_web.ex
@@ -89,8 +89,8 @@ defmodule BodhiWeb do
import BodhiWeb.Components.Seo
# Common modules used in templates
- alias Phoenix.LiveView.JS
alias BodhiWeb.Layouts
+ alias Phoenix.LiveView.JS
# Routes generation with the ~p sigil
unquote(verified_routes())
diff --git a/lib/bodhi_web/live/llm_config_live/form.ex b/lib/bodhi_web/live/llm_config_live/form.ex
index 8ec2559..e4e9585 100644
--- a/lib/bodhi_web/live/llm_config_live/form.ex
+++ b/lib/bodhi_web/live/llm_config_live/form.ex
@@ -48,11 +48,9 @@ defmodule BodhiWeb.LlmConfigLive.Form do
type="number"
label="Max Tokens"
/>
- <.input
- field={@form[:active]}
- type="checkbox"
- label="Active"
- />
+
+ Save this model, then select it from the model list.
+