feat(captain): EmbeddingService aceita provider de embedding dedicado

Permite trocar provider de embedding sem afetar o provider de chat. Útil
quando OpenAI key tradicional está fora (ban, billing, etc) mas você
quer usar outro provider OpenAI-compatible só pra embeddings — exemplo
clássico: Gemini OpenAI-compatible em
https://generativelanguage.googleapis.com/v1beta/openai com modelo
gemini-embedding-001 + dimensions=1536 (pra bater com schema pgvector).

Env vars novas (com fallback pro legacy_openai_settings se não setadas):

  CAPTAIN_EMBEDDING_API_KEY     — API key dedicada pra embeddings
  CAPTAIN_EMBEDDING_ENDPOINT    — base URL sem /v1 (default herda OpenAI)
  CAPTAIN_EMBEDDING_DIMENSIONS  — força redução do vector (ex: 1536)

Quando CAPTAIN_EMBEDDING_API_KEY está vazia, comportamento é idêntico ao
de antes (legacy_openai_settings). Backward-compatible.

Também aceita as variáveis via InstallationConfig (UI) ou ENV — ENV tem
precedência (padrão Chatwoot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rodribm10 2026-05-01 16:08:18 -03:00
parent 8fab08ba57
commit 2ade066468

View File

@ -26,17 +26,47 @@ class Captain::Llm::EmbeddingService
private
# Embeddings sempre vão direto pra OpenAI tradicional — o endpoint Codex
# via ChatGPT OAuth não expõe /embeddings.
# Embeddings vão pra OpenAI tradicional por default (o endpoint Codex
# via ChatGPT OAuth não expõe /embeddings). Override opcional via env vars
# dedicadas — útil pra trocar provider de embedding (ex: Gemini
# OpenAI-compatible) sem alterar o provider de chat:
#
# CAPTAIN_EMBEDDING_API_KEY — sobrescreve API key
# CAPTAIN_EMBEDDING_ENDPOINT — sobrescreve base URL (sem /v1 no final)
# CAPTAIN_EMBEDDING_DIMENSIONS — força reduction (ex: 1536 pra Gemini
# bater com schema pgvector(1536))
def embed_with_legacy_openai(content, model)
legacy = Captain::Llm::ProviderConfig.legacy_openai_settings
api_base = legacy[:api_base].present? ? "#{legacy[:api_base]}/v1" : nil
settings = embedding_settings
api_base = settings[:api_base].present? ? "#{settings[:api_base]}/v1" : nil
embed_options = embed_extra_options
Llm::Config.with_api_key(legacy[:api_key], api_base: api_base) do |ctx|
ctx.embed(content, model: model).vectors
Llm::Config.with_api_key(settings[:api_key], api_base: api_base) do |ctx|
ctx.embed(content, model: model, **embed_options).vectors
end
end
def embedding_settings
custom_key = installation_config_value('CAPTAIN_EMBEDDING_API_KEY')
return Captain::Llm::ProviderConfig.legacy_openai_settings if custom_key.blank?
{
api_key: custom_key,
api_base: installation_config_value('CAPTAIN_EMBEDDING_ENDPOINT')&.chomp('/')
}
end
def embed_extra_options
dims = installation_config_value('CAPTAIN_EMBEDDING_DIMENSIONS')
return {} if dims.blank?
{ dimensions: dims.to_i }
end
def installation_config_value(name)
ENV.fetch(name, nil) ||
InstallationConfig.find_by(name: name)&.value
end
def instrumentation_params(content, model)
{
span_name: 'llm.captain.embedding',