mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 04:03:45 +08:00
## Summary - The "Run Test" button on the Discourse AI LLM admin form only issued a non-streaming completion, so configurations that worked for that probe could still fail in production when a streaming response was requested. - `LlmValidator#run_test` now runs the non-streaming probe followed by a streaming probe (block-form `Llm#generate`) and raises if either returns nothing. It tracks `last_failed_mode` (`:non_streaming` / `:streaming`). - The controller surfaces `failed_mode` in the JSON response and the admin form renders a mode-specific error string so operators can see which path is broken. - Per-user rate limit on `POST /admin/plugins/discourse-ai/ai-llms/test` doubled (3 → 6 / minute) to absorb the extra request per click. ## Test plan - [ ] `bin/rspec plugins/discourse-ai/spec/configuration/llm_validator_spec.rb` - [ ] `bin/rspec plugins/discourse-ai/spec/requests/admin/ai_llms_controller_spec.rb -e "POST #test"` - [ ] In admin UI, click "Run test" on an LLM config and confirm both probes are exercised (success path). - [ ] Configure an endpoint where streaming is broken and confirm the form reports "Streaming request failed: …".
104 lines
2.6 KiB
Ruby
Vendored
104 lines
2.6 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscourseAi
|
|
module Configuration
|
|
class LlmValidator
|
|
TEST_PROMPT = "How much is 1 + 1?"
|
|
|
|
attr_reader :last_failed_mode
|
|
|
|
def initialize(opts = {})
|
|
@opts = opts
|
|
end
|
|
|
|
def valid_value?(val)
|
|
if val == ""
|
|
if @opts[:name] == :ai_default_llm_model
|
|
@parent_module_names = []
|
|
|
|
enabled_settings.each do |setting_name|
|
|
if SiteSetting.public_send(setting_name) == true
|
|
@parent_module_names << setting_name
|
|
@parent_enabled = true
|
|
end
|
|
end
|
|
|
|
return !@parent_enabled
|
|
end
|
|
end
|
|
|
|
run_test(val).tap { |result| @unreachable = result }
|
|
rescue StandardError => e
|
|
raise e if Rails.env.test?
|
|
@unreachable = true
|
|
true
|
|
end
|
|
|
|
def run_test(val)
|
|
llm = DiscourseAi::Completions::Llm.proxy(val)
|
|
|
|
@last_failed_mode = :non_streaming
|
|
raise empty_response_error if probe(llm).blank?
|
|
|
|
@last_failed_mode = :streaming
|
|
streamed = +""
|
|
probe(llm) { |partial| streamed << partial.to_s if partial.is_a?(String) }
|
|
raise empty_response_error if streamed.blank?
|
|
|
|
@last_failed_mode = nil
|
|
true
|
|
end
|
|
|
|
def is_using(llm_model)
|
|
in_use_by = AiAgent.where(default_llm_id: llm_model.id).pluck(:name)
|
|
|
|
in_use_by << "ai_default_llm_model" if SiteSetting.ai_default_llm_model.to_i == llm_model.id
|
|
|
|
in_use_by
|
|
end
|
|
|
|
def error_message
|
|
if @parent_enabled && @parent_module_names.present?
|
|
return(
|
|
I18n.t(
|
|
"discourse_ai.llm.configuration.disable_modules_first",
|
|
settings: @parent_module_names.join(", "),
|
|
)
|
|
)
|
|
end
|
|
|
|
return unless @unreachable
|
|
|
|
I18n.t("discourse_ai.llm.configuration.model_unreachable")
|
|
end
|
|
|
|
def enabled_settings
|
|
%i[
|
|
ai_embeddings_semantic_search_enabled
|
|
ai_helper_enabled
|
|
ai_summarization_enabled
|
|
ai_translation_enabled
|
|
]
|
|
end
|
|
|
|
private
|
|
|
|
def probe(llm, &blk)
|
|
llm.generate(
|
|
TEST_PROMPT,
|
|
user: @opts[:user] || Discourse.system_user,
|
|
feature_name: "llm_validator",
|
|
temperature: 0.7,
|
|
top_p: 0.9,
|
|
&blk
|
|
)
|
|
end
|
|
|
|
def empty_response_error
|
|
DiscourseAi::Completions::Endpoints::Base::CompletionFailed.new(
|
|
I18n.t("discourse_ai.llm.configuration.empty_response"),
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|