# 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