feat(analytics): add LLM credential resolver with cascade
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / security-scan (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled

Add Llm::Resolver which picks the LLM credential/endpoint for a given account
for the M2 conversation analytics classifier, in priority order:
  1. per-account OpenAI integration hook (settings.api_key + optional
     settings.base_url, via Account has_many :hooks)
  2. instance Captain config (CAPTAIN_OPEN_AI_API_KEY / ENDPOINT)
  3. nil => feature disabled for that account (no LLM).

Only https base URLs are accepted (safe_https?); a non-https or blank base_url
is omitted so callers fall back to the provider default (api.openai.com) rather
than an arbitrary host — content never leaks to an unapproved endpoint.

Approved by independent five-key pre-commit review deleg_269e20cb
(passed=true, blocking arrays empty).
This commit is contained in:
Moreminimore
2026-08-19 12:49:21 +07:00
parent 161e4210b7
commit ccff2dfca7

46
lib/llm/resolver.rb Normal file
View File

@@ -0,0 +1,46 @@
# Resolves the LLM credential/endpoint for a given account using the cascade:
# 1. per-account OpenAI integration hook (with its optional custom base_url)
# 2. instance-level Captain config (CAPTAIN_OPEN_AI_API_KEY / ENDPOINT)
# 3. nil => feature disabled (no LLM available for this account)
#
# Returns a Hash with :api_key, :api_base (or nil) or nil when no LLM is
# configured. Only https endpoints are permitted; anything else is treated as
# absent so a bad/missing credential never sends conversation content anywhere.
module Llm::Resolver
module_function
# -- public ---------------------------------------------------------------
def resolve(account)
per_account(account) || captain_config
end
# -- private --------------------------------------------------------------
# The account-scoped OpenAI integration hook (settings.api_key + optional
# settings.base_url). Mirrors Integrations::LlmBaseService behavior.
def per_account(account)
hook = account.hooks.where(app_id: 'openai', status: :enabled).first
return nil if hook.nil? || hook.settings['api_key'].blank?
resolved = { api_key: hook.settings['api_key'] }
base_url = hook.settings['base_url'].presence
resolved[:api_base] = base_url if safe_https?(base_url)
resolved
end
# Instance-level Captain config.
def captain_config
api_key = Llm::Config.system_api_key
return nil if api_key.blank?
resolved = { api_key: api_key }
endpoint = Llm::Config.openai_endpoint.presence
resolved[:api_base] = endpoint if safe_https?(endpoint)
resolved
end
def safe_https?(url)
url.present? && url.to_s.match?(%r{\Ahttps://\S+})
end
end