feat(ai): self-host settings UI for Anthropic provider (5/5)

Adds the Anthropic panel and the install-wide LLM provider selector to
the self-hosting settings page, plus a shared data-retention
disclosure that covers both OpenAI and Anthropic.

- New _llm_provider_selector partial: select for Setting.llm_provider
  (openai | anthropic), respects the LLM_PROVIDER env var (disables the
  control + shows the "configured through environment variables" hint
  when set, mirroring the existing OpenAI panel behaviour), and renders
  a compact data-handling block with one-line retention statements for
  each provider.
- New _anthropic_settings partial mirrors _openai_settings exactly:
  password-field for the API key with **** redaction, optional
  base_url (for AWS Bedrock / GCP Vertex), optional default model. All
  three fields disable when their ENV var is set.
- show.html.erb renders provider selector + OpenAI panel + Anthropic
  panel under the same "General" section so users can configure either
  (or both) without switching pages.
- Settings::HostingsController#update now permits and persists
  anthropic_access_token (ignoring the **** placeholder, same pattern
  as OpenAI), anthropic_base_url, anthropic_model, and llm_provider
  (validated against %w[openai anthropic]). On Setting::ValidationError
  the rescue branch preserves anthropic_base_url / anthropic_model
  input so the form re-renders with the user's typed values intact —
  parity with the issue #1824 fix for OpenAI.
- Locale keys added under settings.hostings.{llm_provider_selector,
  anthropic_settings}.

Tests cover token update + placeholder redaction, base_url + model
update, llm_provider switch to anthropic, and rejection of unknown
provider values. The existing GET render test still passes, exercising
all three new partials.

Closes the 5/5 Anthropic series stacked on #1986.
This commit is contained in:
Guillem Arias
2026-05-25 16:54:22 +02:00
parent 566dd75c27
commit c81055ea58
6 changed files with 185 additions and 1 deletions

View File

@@ -166,6 +166,28 @@ class Settings::HostingsController < ApplicationController
Setting.openai_json_mode = hosting_params[:openai_json_mode].presence
end
if hosting_params.key?(:anthropic_access_token)
token_param = hosting_params[:anthropic_access_token].to_s.strip
unless token_param.blank? || token_param == "********"
Setting.anthropic_access_token = token_param
end
end
if hosting_params.key?(:anthropic_base_url)
Setting.anthropic_base_url = hosting_params[:anthropic_base_url].presence
end
if hosting_params.key?(:anthropic_model)
Setting.anthropic_model = hosting_params[:anthropic_model].presence
end
if hosting_params.key?(:llm_provider)
provider = hosting_params[:llm_provider].to_s
if %w[openai anthropic].include?(provider)
Setting.llm_provider = provider
end
end
LLM_BUDGET_MINIMUMS.each do |key, minimum|
next unless hosting_params.key?(key)
raw = hosting_params[key].to_s.strip
@@ -206,6 +228,8 @@ class Settings::HostingsController < ApplicationController
# be wiped because the view reads from the unchanged Setting.* values.
@openai_uri_base_input = hosting_params[:openai_uri_base] if hosting_params.key?(:openai_uri_base)
@openai_model_input = hosting_params[:openai_model] if hosting_params.key?(:openai_model)
@anthropic_base_url_input = hosting_params[:anthropic_base_url] if hosting_params.key?(:anthropic_base_url)
@anthropic_model_input = hosting_params[:anthropic_model] if hosting_params.key?(:anthropic_model)
flash.now[:alert] = error.message
render :show, status: :unprocessable_entity
end
@@ -229,7 +253,7 @@ class Settings::HostingsController < ApplicationController
private
def hosting_params
return ActionController::Parameters.new unless params.key?(:setting)
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :invite_only_default_family_id, :brand_fetch_client_id, :brand_fetch_high_res_logos, :twelve_data_api_key, :tiingo_api_key, :eodhd_api_key, :alpha_vantage_api_key, :openai_access_token, :openai_uri_base, :openai_model, :openai_json_mode, :llm_context_window, :llm_max_response_tokens, :llm_max_items_per_call, :exchange_rate_provider, :securities_provider, :syncs_include_pending, :auto_sync_enabled, :auto_sync_time, :external_assistant_url, :external_assistant_token, :external_assistant_agent_id, securities_providers: [])
params.require(:setting).permit(:onboarding_state, :require_email_confirmation, :invite_only_default_family_id, :brand_fetch_client_id, :brand_fetch_high_res_logos, :twelve_data_api_key, :tiingo_api_key, :eodhd_api_key, :alpha_vantage_api_key, :openai_access_token, :openai_uri_base, :openai_model, :openai_json_mode, :anthropic_access_token, :anthropic_base_url, :anthropic_model, :llm_provider, :llm_context_window, :llm_max_response_tokens, :llm_max_items_per_call, :exchange_rate_provider, :securities_provider, :syncs_include_pending, :auto_sync_enabled, :auto_sync_time, :external_assistant_url, :external_assistant_token, :external_assistant_agent_id, securities_providers: [])
end
def update_assistant_type