feat(openai): add optional custom base URL to the OpenAI integration
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 / 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
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
Lock Threads / action (push) Has been cancelled
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 / 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
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
Lock Threads / action (push) Has been cancelled
Add an optional custom OpenAI-compatible base URL to the per-account OpenAI
integration (/app/accounts/{id}/settings/integrations/openai) so account owners
can point label suggestions / reply suggestions at a self-hosted or proxy
endpoint (vLLM, OpenRouter, etc.) instead of the default api.openai.com.
- config/integration/apps.yml: add 'base_url' to the openai settings schema +
form (optional custom URL input).
- lib/integrations/llm_base_service.rb: api_base resolves per-account
hook.settings['base_url'] -> CAPTAIN_OPEN_AI_ENDPOINT -> api.openai.com.
- lib/integrations/openai/key_validator.rb: valid? accepts an optional base_url
keyword; api_base uses it first (backward-compatible).
- app/models/integrations/hook.rb: fail-closed validation rejecting a present
but non-https base_url; both KeyValidator callers pass the hook base_url.
Approved by independent five-key pre-commit review deleg_1c66a89b
(passed=true, blocking arrays empty).
This commit is contained in:
@@ -9,7 +9,7 @@ class Migration::ValidateOpenaiHooksJob < ApplicationJob
|
||||
|
||||
scope.find_each do |hook|
|
||||
stats[:checked] += 1
|
||||
next if Integrations::Openai::KeyValidator.valid?(hook.settings&.dig('api_key'))
|
||||
next if Integrations::Openai::KeyValidator.valid?(hook.settings&.dig('api_key'), base_url: hook.settings&.dig('base_url'))
|
||||
|
||||
AdministratorNotifications::IntegrationsNotificationMailer.with(account: hook.account).openai_disconnect.deliver_later
|
||||
hook.destroy!
|
||||
|
||||
@@ -30,6 +30,7 @@ class Integrations::Hook < ApplicationRecord
|
||||
validate :validate_settings_json_schema
|
||||
validate :ensure_feature_enabled
|
||||
validate :validate_openai_api_key, if: :validate_openai_api_key?
|
||||
validate :validate_openai_base_url, if: :validate_openai_base_url?
|
||||
validate :validate_cloudflare_realtimekit_credentials, if: :validate_cloudflare_realtimekit_credentials?
|
||||
validates :app_id, uniqueness: { scope: [:account_id], unless: -> { app.present? && app.params[:allow_multiple_hooks].present? } }
|
||||
|
||||
@@ -114,6 +115,19 @@ class Integrations::Hook < ApplicationRecord
|
||||
openai? && enabled? && (new_record? || openai_api_key_changed? || will_save_change_to_status?)
|
||||
end
|
||||
|
||||
def validate_openai_base_url?
|
||||
openai? && enabled?
|
||||
end
|
||||
|
||||
def validate_openai_base_url
|
||||
base_url = settings.dig('base_url')
|
||||
return if base_url.blank?
|
||||
|
||||
return if base_url.to_s.match?(%r{\Ahttps://\S+})
|
||||
|
||||
errors.add(:base, 'OpenAI base URL must be a valid https URL')
|
||||
end
|
||||
|
||||
def validate_cloudflare_realtimekit_credentials?
|
||||
dyte? && enabled? && !legacy_dyte_settings_unchanged? &&
|
||||
(new_record? || cloudflare_realtimekit_credentials_changed? || will_save_change_to_status?)
|
||||
@@ -139,7 +153,7 @@ class Integrations::Hook < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_openai_api_key
|
||||
return if Integrations::Openai::KeyValidator.valid?(settings_api_key(settings))
|
||||
return if Integrations::Openai::KeyValidator.valid?(settings_api_key(settings), base_url: settings.dig('base_url'))
|
||||
|
||||
errors.add(:base, I18n.t('errors.openai.invalid_api_key'))
|
||||
end
|
||||
|
||||
@@ -36,6 +36,7 @@ openai:
|
||||
'properties':
|
||||
{
|
||||
'api_key': { 'type': 'string' },
|
||||
'base_url': { 'type': 'string' },
|
||||
'label_suggestion': { 'type': 'boolean' },
|
||||
},
|
||||
'required': ['api_key'],
|
||||
@@ -49,6 +50,12 @@ openai:
|
||||
'name': 'api_key',
|
||||
'validation': 'required',
|
||||
},
|
||||
{
|
||||
'label': 'OpenAI Base URL (custom OpenAI-compatible server, optional)',
|
||||
'type': 'text',
|
||||
'name': 'base_url',
|
||||
'validation': '',
|
||||
},
|
||||
{
|
||||
'label': 'Show label suggestions',
|
||||
'type': 'checkbox',
|
||||
|
||||
@@ -83,8 +83,14 @@ class Integrations::LlmBaseService
|
||||
self.class::CACHEABLE_EVENTS.include?(event_name)
|
||||
end
|
||||
|
||||
# Resolve the OpenAI-compatible API base URL in priority order:
|
||||
# 1. Per-account custom base_url set on the OpenAI integration hook (if any)
|
||||
# 2. Instance-level CAPTAIN_OPEN_AI_ENDPOINT (if any)
|
||||
# 3. Default https://api.openai.com/
|
||||
def api_base
|
||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
|
||||
endpoint = hook.settings['base_url'].presence ||
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence ||
|
||||
'https://api.openai.com/'
|
||||
endpoint = endpoint.chomp('/')
|
||||
"#{endpoint}/v1"
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Integrations::Openai::KeyValidator
|
||||
TIMEOUT_SECONDS = 5
|
||||
|
||||
def self.valid?(api_key)
|
||||
def self.valid?(api_key, base_url: nil)
|
||||
return false if api_key.blank?
|
||||
|
||||
connection = Faraday.new do |f|
|
||||
@@ -9,7 +9,7 @@ module Integrations::Openai::KeyValidator
|
||||
f.options.open_timeout = TIMEOUT_SECONDS
|
||||
end
|
||||
|
||||
response = connection.get("#{api_base}/models") do |req|
|
||||
response = connection.get("#{api_base(base_url)}/models") do |req|
|
||||
req.headers['Authorization'] = "Bearer #{api_key}"
|
||||
end
|
||||
|
||||
@@ -19,8 +19,12 @@ module Integrations::Openai::KeyValidator
|
||||
true
|
||||
end
|
||||
|
||||
def self.api_base
|
||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
|
||||
# Resolve the API base URL: prefer an explicit per-account custom base_url,
|
||||
# then the instance-level CAPTAIN_OPEN_AI_ENDPOINT, else default api.openai.com.
|
||||
def self.api_base(base_url = nil)
|
||||
endpoint = base_url.presence ||
|
||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence ||
|
||||
'https://api.openai.com/'
|
||||
"#{endpoint.chomp('/')}/v1"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user