feat(captain): add OpenAI Custom Server provider option
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
Sync GHSA advisories to Linear / sync (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
Sync GHSA advisories to Linear / sync (push) Has been cancelled
Add an operator-level provider switch for Captain AI so it can use any OpenAI-compatible base URL (vLLM, OpenRouter, proxies, etc.) in addition to the default api.openai.com endpoint. - installation_config.yml: new CAPTAIN_OPEN_AI_PROVIDER (default 'openai', select openai|custom) shown on the Super Admin captain settings page. - lib/llm/config.rb: openai_endpoint now returns the configured CAPTAIN_OPEN_AI_ENDPOINT only when provider=='custom'; nil for openai/default so a stale endpoint is ignored. system_api_key/openai_endpoint made public so all consumers share one provider-switch source. - Consumers (base_task_service, llm_base_service, key_validator, ai_agents initializer) now route via Llm::Config.openai_endpoint instead of reading CAPTAIN_OPEN_AI_ENDPOINT directly. - installation_config.rb: fail-closed validation (provider must be openai/custom; provider=custom requires a present https:// endpoint). Approved by independent five-key reviews deleg_b141098a (found+fixed one logic gap) and deleg_3088b922 (passed=true, blocking arrays empty).
This commit is contained in:
@@ -56,7 +56,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
|||||||
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
|
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
|
||||||
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
|
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
|
||||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN],
|
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN],
|
||||||
'captain' => %w[CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL CAPTAIN_OPEN_AI_ENDPOINT]
|
'captain' => %w[CAPTAIN_OPEN_AI_PROVIDER CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL CAPTAIN_OPEN_AI_ENDPOINT]
|
||||||
}
|
}
|
||||||
|
|
||||||
@allowed_configs = mapping.fetch(@config, general_configs)
|
@allowed_configs = mapping.fetch(@config, general_configs)
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ class InstallationConfig < ApplicationRecord
|
|||||||
before_validation :set_lock
|
before_validation :set_lock
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validate :saml_sso_users_check, if: -> { name == 'ENABLE_SAML_SSO_LOGIN' }
|
validate :saml_sso_users_check, if: -> { name == 'ENABLE_SAML_SSO_LOGIN' }
|
||||||
|
validate :openai_custom_provider_check, if: -> { name == 'CAPTAIN_OPEN_AI_PROVIDER' }
|
||||||
|
validate :openai_custom_endpoint_check, if: -> { name == 'CAPTAIN_OPEN_AI_ENDPOINT' }
|
||||||
|
|
||||||
# TODO: Get rid of default scope
|
# TODO: Get rid of default scope
|
||||||
# https://stackoverflow.com/a/1834250/939299
|
# https://stackoverflow.com/a/1834250/939299
|
||||||
@@ -71,4 +73,28 @@ class InstallationConfig < ApplicationRecord
|
|||||||
|
|
||||||
errors.add(:base, 'Cannot disable SAML SSO login while users are using SAML authentication')
|
errors.add(:base, 'Cannot disable SAML SSO login while users are using SAML authentication')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Only allow provider values the platform understands ('openai' default or 'custom')
|
||||||
|
# and ensure the "custom" provider actually requires an endpoint.
|
||||||
|
def openai_custom_provider_check
|
||||||
|
return if %w[openai custom].include?(value.to_s)
|
||||||
|
|
||||||
|
errors.add(:base, 'CAPTAIN_OPEN_AI_PROVIDER must be "openai" or "custom"')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fail-closed: when the provider is "custom", CAPTAIN_OPEN_AI_ENDPOINT must be
|
||||||
|
# present AND a secure https URL (OpenAI-compatible base).
|
||||||
|
def openai_custom_endpoint_check
|
||||||
|
provider = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_PROVIDER')&.value.to_s
|
||||||
|
return unless provider == 'custom'
|
||||||
|
|
||||||
|
if value.blank?
|
||||||
|
errors.add(:base, 'CAPTAIN_OPEN_AI_ENDPOINT is required when using the custom provider')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return if value.to_s.match?(%r{\Ahttps://\S+})
|
||||||
|
|
||||||
|
errors.add(:base, 'CAPTAIN_OPEN_AI_ENDPOINT must be a valid https URL when using the custom provider')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ require 'agents'
|
|||||||
Rails.application.config.after_initialize do
|
Rails.application.config.after_initialize do
|
||||||
api_key = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
api_key = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
||||||
model = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value.presence || LlmConstants::DEFAULT_MODEL
|
model = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_MODEL')&.value.presence || LlmConstants::DEFAULT_MODEL
|
||||||
api_endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value || LlmConstants::OPENAI_API_ENDPOINT
|
api_endpoint = Llm::Config.openai_endpoint.presence || LlmConstants::OPENAI_API_ENDPOINT
|
||||||
|
|
||||||
if api_key.present?
|
if api_key.present?
|
||||||
Agents.configure do |config|
|
Agents.configure do |config|
|
||||||
|
|||||||
@@ -191,6 +191,15 @@
|
|||||||
display_title: 'OpenAI API Endpoint (optional)'
|
display_title: 'OpenAI API Endpoint (optional)'
|
||||||
description: 'The OpenAI endpoint configured for use in Captain AI. Default: https://api.openai.com/'
|
description: 'The OpenAI endpoint configured for use in Captain AI. Default: https://api.openai.com/'
|
||||||
locked: false
|
locked: false
|
||||||
|
- name: CAPTAIN_OPEN_AI_PROVIDER
|
||||||
|
display_title: 'OpenAI Provider'
|
||||||
|
description: 'Select the OpenAI-compatible provider: "openai" uses the default api.openai.com endpoint; "custom" lets you set a custom OpenAI-compatible base URL in CAPTAIN_OPEN_AI_ENDPOINT.'
|
||||||
|
locked: false
|
||||||
|
value: openai
|
||||||
|
type: select
|
||||||
|
options:
|
||||||
|
openai: 'OpenAI (default: api.openai.com)'
|
||||||
|
custom: 'OpenAI Custom Server'
|
||||||
- name: CAPTAIN_EMBEDDING_MODEL
|
- name: CAPTAIN_EMBEDDING_MODEL
|
||||||
display_title: 'Embedding Model (optional)'
|
display_title: 'Embedding Model (optional)'
|
||||||
description: 'The embedding model configured for use in Captain AI. Default: text-embedding-3-small'
|
description: 'The embedding model configured for use in Captain AI. Default: text-embedding-3-small'
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Captain::BaseTaskService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def api_base
|
def api_base
|
||||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
|
endpoint = Llm::Config.openai_endpoint.presence || 'https://api.openai.com/'
|
||||||
endpoint = endpoint.chomp('/')
|
endpoint = endpoint.chomp('/')
|
||||||
"#{endpoint}/v1"
|
"#{endpoint}/v1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class Integrations::LlmBaseService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def api_base
|
def api_base
|
||||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
|
endpoint = Llm::Config.openai_endpoint.presence || 'https://api.openai.com/'
|
||||||
endpoint = endpoint.chomp('/')
|
endpoint = endpoint.chomp('/')
|
||||||
"#{endpoint}/v1"
|
"#{endpoint}/v1"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ module Integrations::Openai::KeyValidator
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.api_base
|
def self.api_base
|
||||||
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
|
endpoint = Llm::Config.openai_endpoint.presence || 'https://api.openai.com/'
|
||||||
"#{endpoint.chomp('/')}/v1"
|
"#{endpoint.chomp('/')}/v1"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ module Llm::Config
|
|||||||
yield context
|
yield context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the OpenAI-compatible base URL for Captain AI.
|
||||||
|
# - provider == 'custom' -> use the operator-configured CAPTAIN_OPEN_AI_ENDPOINT
|
||||||
|
# - provider == 'openai' -> nil (RubyLLM uses the default https://api.openai.com/)
|
||||||
|
def openai_endpoint
|
||||||
|
provider = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_PROVIDER')&.value.presence || 'openai'
|
||||||
|
return nil unless provider == 'custom'
|
||||||
|
|
||||||
|
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def system_api_key
|
||||||
|
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def configure_ruby_llm
|
def configure_ruby_llm
|
||||||
@@ -39,13 +53,5 @@ module Llm::Config
|
|||||||
config.logger = Rails.logger
|
config.logger = Rails.logger
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def system_api_key
|
|
||||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_API_KEY')&.value
|
|
||||||
end
|
|
||||||
|
|
||||||
def openai_endpoint
|
|
||||||
InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user