mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
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:
@@ -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
|
||||
|
||||
53
app/views/settings/hostings/_anthropic_settings.html.erb
Normal file
53
app/views/settings/hostings/_anthropic_settings.html.erb
Normal file
@@ -0,0 +1,53 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
||||
<% if ENV["ANTHROPIC_ACCESS_TOKEN"].present? || ENV["ANTHROPIC_API_KEY"].present? %>
|
||||
<p class="text-sm text-secondary"><%= t(".env_configured_message") %></p>
|
||||
<% else %>
|
||||
<p class="text-secondary text-sm mb-4"><%= t(".description") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: Setting.new,
|
||||
url: settings_hosting_path,
|
||||
method: :patch,
|
||||
class: "space-y-4",
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
"auto-submit-form-trigger-event-value": "blur"
|
||||
} do |form| %>
|
||||
<%= form.password_field :anthropic_access_token,
|
||||
label: t(".access_token_label"),
|
||||
placeholder: t(".access_token_placeholder"),
|
||||
value: (Setting.anthropic_access_token.present? ? "********" : nil),
|
||||
autocomplete: "off",
|
||||
autocapitalize: "none",
|
||||
spellcheck: "false",
|
||||
inputmode: "text",
|
||||
disabled: ENV["ANTHROPIC_ACCESS_TOKEN"].present? || ENV["ANTHROPIC_API_KEY"].present?,
|
||||
data: { "auto-submit-form-target": "auto" } %>
|
||||
|
||||
<%= form.text_field :anthropic_base_url,
|
||||
label: t(".base_url_label"),
|
||||
placeholder: t(".base_url_placeholder"),
|
||||
value: @anthropic_base_url_input || Setting.anthropic_base_url,
|
||||
autocomplete: "off",
|
||||
autocapitalize: "none",
|
||||
spellcheck: "false",
|
||||
inputmode: "url",
|
||||
disabled: ENV["ANTHROPIC_BASE_URL"].present?,
|
||||
data: { "auto-submit-form-target": "auto" } %>
|
||||
|
||||
<%= form.text_field :anthropic_model,
|
||||
label: t(".model_label"),
|
||||
placeholder: t(".model_placeholder"),
|
||||
value: @anthropic_model_input || Setting.anthropic_model,
|
||||
autocomplete: "off",
|
||||
autocapitalize: "none",
|
||||
spellcheck: "false",
|
||||
inputmode: "text",
|
||||
disabled: ENV["ANTHROPIC_MODEL"].present?,
|
||||
data: { "auto-submit-form-target": "auto" } %>
|
||||
<p class="text-xs text-secondary mt-1"><%= t(".model_help") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
38
app/views/settings/hostings/_llm_provider_selector.html.erb
Normal file
38
app/views/settings/hostings/_llm_provider_selector.html.erb
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<h2 class="font-medium mb-1"><%= t(".title") %></h2>
|
||||
<% if ENV["LLM_PROVIDER"].present? %>
|
||||
<p class="text-sm text-secondary"><%= t(".env_configured_message") %></p>
|
||||
<% else %>
|
||||
<p class="text-secondary text-sm"><%= t(".description") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= styled_form_with model: Setting.new,
|
||||
url: settings_hosting_path,
|
||||
method: :patch,
|
||||
class: "space-y-3",
|
||||
data: {
|
||||
controller: "auto-submit-form",
|
||||
"auto-submit-form-trigger-event-value": "change"
|
||||
} do |form| %>
|
||||
<%= form.select :llm_provider,
|
||||
options_for_select(
|
||||
[
|
||||
[ t(".provider_openai"), "openai" ],
|
||||
[ t(".provider_anthropic"), "anthropic" ]
|
||||
],
|
||||
Setting.llm_provider
|
||||
),
|
||||
{ label: t(".provider_label") },
|
||||
{ disabled: ENV["LLM_PROVIDER"].present?,
|
||||
data: { "auto-submit-form-target": "auto" } } %>
|
||||
<p class="text-xs text-secondary"><%= t(".provider_help") %></p>
|
||||
<% end %>
|
||||
|
||||
<div class="rounded-md border border-secondary p-3 bg-surface-secondary text-xs text-secondary space-y-2">
|
||||
<p class="font-medium text-primary"><%= t(".data_retention_heading") %></p>
|
||||
<p><%= t(".data_retention_openai") %></p>
|
||||
<p><%= t(".data_retention_anthropic") %></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,9 @@
|
||||
<% end %>
|
||||
<%= settings_section title: t(".general") do %>
|
||||
<div class="space-y-6">
|
||||
<%= render "settings/hostings/llm_provider_selector" %>
|
||||
<%= render "settings/hostings/openai_settings" %>
|
||||
<%= render "settings/hostings/anthropic_settings" %>
|
||||
<%= render "settings/hostings/brand_fetch_settings" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -90,6 +90,28 @@ en:
|
||||
title: Brand Fetch Settings
|
||||
high_res_label: Enable high-resolution logos
|
||||
high_res_description: When enabled, logos will be retrieved at 120x120 resolution instead of 40x40. This provides sharper images on high-DPI displays.
|
||||
llm_provider_selector:
|
||||
title: AI Provider
|
||||
description: Choose which LLM powers chat, transaction categorization, merchant detection, and PDF processing.
|
||||
env_configured_message: Successfully configured through the LLM_PROVIDER environment variable.
|
||||
provider_label: Active LLM provider
|
||||
provider_openai: OpenAI
|
||||
provider_anthropic: Anthropic (Claude)
|
||||
provider_help: Switching providers takes effect on the next chat or batch operation. Make sure the chosen provider's credentials are configured below.
|
||||
data_retention_heading: Data handling
|
||||
data_retention_openai: "OpenAI: API inputs are not used to train models by default. Standard retention is 30 days for trust & safety."
|
||||
data_retention_anthropic: "Anthropic: API inputs are not used to train models by default. Standard retention is 30 days for trust & safety."
|
||||
anthropic_settings:
|
||||
title: Anthropic (Claude)
|
||||
description: Enter your Anthropic API key. Optionally point Base URL at AWS Bedrock or GCP Vertex.
|
||||
env_configured_message: Successfully configured through environment variables.
|
||||
access_token_label: API Key
|
||||
access_token_placeholder: Enter your Anthropic API key
|
||||
base_url_label: Base URL (Optional)
|
||||
base_url_placeholder: "https://api.anthropic.com (default)"
|
||||
model_label: Default Model (Optional)
|
||||
model_placeholder: "claude-sonnet-4-6 (default)"
|
||||
model_help: Used for chat and PDF processing. Batch operations (categorize, merchant detection) default to Haiku for cost.
|
||||
openai_settings:
|
||||
description: Enter the access token and optionally configure a custom OpenAI-compatible provider
|
||||
env_configured_message: Successfully configured through environment variables.
|
||||
|
||||
@@ -74,6 +74,51 @@ class Settings::HostingsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
end
|
||||
|
||||
test "can update anthropic access token when self hosting is enabled" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { anthropic_access_token: "sk-ant-test" } }
|
||||
|
||||
assert_equal "sk-ant-test", Setting.anthropic_access_token
|
||||
end
|
||||
end
|
||||
|
||||
test "ignores redacted anthropic token placeholder" do
|
||||
with_self_hosting do
|
||||
Setting.anthropic_access_token = "previous-token"
|
||||
|
||||
patch settings_hosting_url, params: { setting: { anthropic_access_token: "********" } }
|
||||
|
||||
assert_equal "previous-token", Setting.anthropic_access_token
|
||||
end
|
||||
end
|
||||
|
||||
test "can update anthropic base_url and model" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { anthropic_base_url: "https://bedrock.example.com", anthropic_model: "claude-opus-4-7" } }
|
||||
|
||||
assert_equal "https://bedrock.example.com", Setting.anthropic_base_url
|
||||
assert_equal "claude-opus-4-7", Setting.anthropic_model
|
||||
end
|
||||
end
|
||||
|
||||
test "can update llm_provider to anthropic" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { llm_provider: "anthropic" } }
|
||||
|
||||
assert_equal "anthropic", Setting.llm_provider
|
||||
end
|
||||
end
|
||||
|
||||
test "rejects unknown llm_provider values" do
|
||||
with_self_hosting do
|
||||
Setting.llm_provider = "openai"
|
||||
|
||||
patch settings_hosting_url, params: { setting: { llm_provider: "bogus" } }
|
||||
|
||||
assert_equal "openai", Setting.llm_provider
|
||||
end
|
||||
end
|
||||
|
||||
test "can update openai uri base and model together when self hosting is enabled" do
|
||||
with_self_hosting do
|
||||
patch settings_hosting_url, params: { setting: { openai_uri_base: "https://api.example.com/v1", openai_model: "gpt-4" } }
|
||||
|
||||
Reference in New Issue
Block a user