diff --git a/lib/llm/resolver.rb b/lib/llm/resolver.rb new file mode 100644 index 000000000..2c43fae36 --- /dev/null +++ b/lib/llm/resolver.rb @@ -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