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