feat(captain): adiciona Hermes Gateway como 3ª opção de LLM provider
Acrescenta valor 'openai_hermes_gateway' ao CAPTAIN_LLM_PROVIDER, sem mexer nas opções existentes (openai_api e openai_codex_oauth continuam intactos). Quando ativado, o Captain chama o Hermes Agent rodando em modo gateway HTTP local (CAPTAIN_HERMES_GATEWAY_URL, default http://host.docker.internal:9877). O Hermes faz o roteamento multi-modelo (Codex/Anthropic/Gemini) usando o OAuth dele em ~/.hermes/auth.json — o Captain não precisa fazer OAuth direto. Configs novas em installation_config.yml: - CAPTAIN_HERMES_GATEWAY_URL — URL do gateway (default host.docker.internal:9877) - CAPTAIN_HERMES_GATEWAY_MODEL — modelo no formato <provider>/<model> - CAPTAIN_HERMES_GATEWAY_API_KEY — opcional, dummy se gateway local não exige Embeddings e Files API continuam apontando pra OpenAI tradicional via legacy_openai_settings — Hermes Gateway não expõe esses endpoints. Specs cobrem: dummy key, custom api_key override, custom model, defaults, trailing slash strip, light_model por provider, hermes_gateway? predicate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b8ef5a828
commit
7700afd508
@ -184,7 +184,7 @@
|
||||
# MARK: Captain Config
|
||||
- name: CAPTAIN_LLM_PROVIDER
|
||||
display_title: 'Captain LLM Provider'
|
||||
description: 'Qual provider o Captain usa: openai_api (padrão, API key tradicional) ou openai_codex_oauth (assinatura ChatGPT Plus via proxy interno).'
|
||||
description: 'Qual provider o Captain usa: openai_api (padrão, API key tradicional), openai_codex_oauth (assinatura ChatGPT Plus via proxy interno) ou openai_hermes_gateway (Hermes Agent rodando como gateway HTTP local — ele faz o roteamento multi-modelo via OAuth).'
|
||||
value: 'openai_api'
|
||||
locked: false
|
||||
- name: CAPTAIN_CODEX_PROXY_URL
|
||||
@ -192,6 +192,21 @@
|
||||
description: 'URL base do proxy Codex interno quando CAPTAIN_LLM_PROVIDER=openai_codex_oauth. Default: http://localhost:3000/codex'
|
||||
value: 'http://localhost:3000/codex'
|
||||
locked: false
|
||||
- name: CAPTAIN_HERMES_GATEWAY_URL
|
||||
display_title: 'Captain Hermes Gateway URL'
|
||||
description: 'URL base do Hermes Gateway quando CAPTAIN_LLM_PROVIDER=openai_hermes_gateway. Default: http://host.docker.internal:9877 (Hermes rodando no host, container alcança via host.docker.internal).'
|
||||
value: 'http://host.docker.internal:9877'
|
||||
locked: false
|
||||
- name: CAPTAIN_HERMES_GATEWAY_MODEL
|
||||
display_title: 'Captain Hermes Gateway Model'
|
||||
description: 'Modelo a passar pro Hermes Gateway no formato <provider>/<model>. Default: anthropic/claude-opus-4-5. O Hermes faz o roteamento real e pode usar Codex/Anthropic/Gemini conforme config local em ~/.hermes/config.yaml.'
|
||||
value: 'anthropic/claude-opus-4-5'
|
||||
locked: false
|
||||
- name: CAPTAIN_HERMES_GATEWAY_API_KEY
|
||||
display_title: 'Captain Hermes Gateway API Key (optional)'
|
||||
description: 'API key opcional pro Hermes Gateway. Geralmente vazio (gateway local não exige auth). Se setado, vai no Authorization header das requisições do Captain pro Hermes.'
|
||||
locked: false
|
||||
type: secret
|
||||
- name: CAPTAIN_OPEN_AI_API_KEY
|
||||
display_title: 'OpenAI API Key'
|
||||
description: 'The API key used to authenticate requests to OpenAI services for Captain AI.'
|
||||
|
||||
@ -9,9 +9,16 @@
|
||||
# (CAPTAIN_CODEX_PROXY_URL, default http://localhost:3000/codex) e usa uma
|
||||
# api_key dummy — o proxy ignora o Authorization header e usa OAuth interno.
|
||||
#
|
||||
# - openai_hermes_gateway: aponta para o Hermes Agent rodando em modo gateway
|
||||
# (CAPTAIN_HERMES_GATEWAY_URL, default http://host.docker.internal:9877).
|
||||
# O Hermes Gateway expõe API HTTP compatível com OpenAI e roteia internamente
|
||||
# pra Codex/Anthropic/Gemini conforme sua config local em ~/.hermes/config.yaml.
|
||||
# Auth: usa CAPTAIN_HERMES_GATEWAY_API_KEY se setado, senão dummy (gateway local).
|
||||
#
|
||||
# O "legacy" ruby-openai usado para PDF/Files API NÃO deve usar esse módulo:
|
||||
# o endpoint Codex não expõe Files API, então esses serviços continuam
|
||||
# apontando sempre para OpenAI tradicional.
|
||||
# o endpoint Codex/Hermes não expõe Files API nem /embeddings, então esses
|
||||
# serviços continuam apontando sempre para OpenAI tradicional via
|
||||
# legacy_openai_settings.
|
||||
class Captain::Llm::ProviderConfig
|
||||
DEFAULT_MODEL = 'gpt-4.1-mini'.freeze
|
||||
DEFAULT_OPENAI_ENDPOINT = 'https://api.openai.com'.freeze
|
||||
@ -23,12 +30,20 @@ class Captain::Llm::ProviderConfig
|
||||
# endpoint Codex da OpenAI via ChatGPT Plus.
|
||||
DEFAULT_CODEX_MODEL = 'gpt-5.2'.freeze
|
||||
|
||||
# Hermes Gateway: defaults para o setup standard do Hermes Agent rodando
|
||||
# como gateway HTTP local. O gateway escuta em 0.0.0.0:9877 por padrão e
|
||||
# aceita o nome do modelo no formato `<provider>/<model>`.
|
||||
DEFAULT_HERMES_GATEWAY_URL = 'http://host.docker.internal:9877'.freeze
|
||||
DEFAULT_HERMES_GATEWAY_MODEL = 'anthropic/claude-opus-4-5'.freeze
|
||||
HERMES_GATEWAY_DUMMY_KEY = 'hermes-gateway'.freeze
|
||||
|
||||
# Modelo leve pra tasks de background (extração de memória, verificação de
|
||||
# contradição, traduções internas). Quando usamos Codex, reutilizamos o
|
||||
# mesmo modelo do chat — o endpoint não expõe gpt-4o-mini.
|
||||
# contradição, traduções internas). Quando usamos Codex/Hermes, reutilizamos
|
||||
# o mesmo modelo do chat — esses endpoints não expõem gpt-4o-mini.
|
||||
LIGHT_MODEL_DEFAULTS = {
|
||||
'openai_api' => 'gpt-4o-mini',
|
||||
'openai_codex_oauth' => DEFAULT_CODEX_MODEL
|
||||
'openai_codex_oauth' => DEFAULT_CODEX_MODEL,
|
||||
'openai_hermes_gateway' => DEFAULT_HERMES_GATEWAY_MODEL
|
||||
}.freeze
|
||||
|
||||
class << self
|
||||
@ -40,12 +55,16 @@ class Captain::Llm::ProviderConfig
|
||||
provider == 'openai_codex_oauth'
|
||||
end
|
||||
|
||||
def hermes_gateway?
|
||||
provider == 'openai_hermes_gateway'
|
||||
end
|
||||
|
||||
# Retorna { api_key:, api_base:, model: } para RubyLLM/Agents.
|
||||
def settings
|
||||
if codex_oauth?
|
||||
codex_settings
|
||||
else
|
||||
openai_api_settings
|
||||
case provider
|
||||
when 'openai_codex_oauth' then codex_settings
|
||||
when 'openai_hermes_gateway' then hermes_gateway_settings
|
||||
else openai_api_settings
|
||||
end
|
||||
end
|
||||
|
||||
@ -88,6 +107,14 @@ class Captain::Llm::ProviderConfig
|
||||
}
|
||||
end
|
||||
|
||||
def hermes_gateway_settings
|
||||
{
|
||||
api_key: cfg('CAPTAIN_HERMES_GATEWAY_API_KEY').presence || HERMES_GATEWAY_DUMMY_KEY,
|
||||
api_base: (cfg('CAPTAIN_HERMES_GATEWAY_URL').presence || DEFAULT_HERMES_GATEWAY_URL).chomp('/'),
|
||||
model: cfg('CAPTAIN_HERMES_GATEWAY_MODEL').presence || DEFAULT_HERMES_GATEWAY_MODEL
|
||||
}
|
||||
end
|
||||
|
||||
def openai_api_settings
|
||||
{
|
||||
api_key: cfg('CAPTAIN_OPEN_AI_API_KEY'),
|
||||
|
||||
@ -63,5 +63,72 @@ RSpec.describe Captain::Llm::ProviderConfig do
|
||||
expect(described_class.settings[:api_base]).to eq(described_class::DEFAULT_CODEX_PROXY_URL)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider is openai_hermes_gateway' do
|
||||
before do
|
||||
InstallationConfig.create!(name: 'CAPTAIN_LLM_PROVIDER', value: 'openai_hermes_gateway')
|
||||
InstallationConfig.create!(name: 'CAPTAIN_HERMES_GATEWAY_URL', value: 'http://host.docker.internal:9877')
|
||||
InstallationConfig.create!(name: 'CAPTAIN_HERMES_GATEWAY_MODEL', value: 'anthropic/claude-opus-4-5')
|
||||
end
|
||||
|
||||
it 'returns the gateway URL with dummy api_key when no key is configured' do
|
||||
settings = described_class.settings
|
||||
expect(settings[:api_key]).to eq(described_class::HERMES_GATEWAY_DUMMY_KEY)
|
||||
expect(settings[:api_base]).to eq('http://host.docker.internal:9877')
|
||||
expect(settings[:model]).to eq('anthropic/claude-opus-4-5')
|
||||
end
|
||||
|
||||
it 'honors CAPTAIN_HERMES_GATEWAY_API_KEY when present' do
|
||||
InstallationConfig.create!(name: 'CAPTAIN_HERMES_GATEWAY_API_KEY', value: 'sk-hermes-real')
|
||||
expect(described_class.settings[:api_key]).to eq('sk-hermes-real')
|
||||
end
|
||||
|
||||
it 'honors a custom CAPTAIN_HERMES_GATEWAY_MODEL value' do
|
||||
InstallationConfig.find_by!(name: 'CAPTAIN_HERMES_GATEWAY_MODEL').update!(value: 'openai/gpt-5.4')
|
||||
expect(described_class.settings[:model]).to eq('openai/gpt-5.4')
|
||||
end
|
||||
|
||||
it 'reports hermes_gateway? as true and codex_oauth? as false' do
|
||||
expect(described_class.hermes_gateway?).to be true
|
||||
expect(described_class.codex_oauth?).to be false
|
||||
end
|
||||
|
||||
it 'strips trailing slash from gateway URL' do
|
||||
InstallationConfig.find_by!(name: 'CAPTAIN_HERMES_GATEWAY_URL').update!(value: 'http://host.docker.internal:9877/')
|
||||
expect(described_class.settings[:api_base]).to eq('http://host.docker.internal:9877')
|
||||
end
|
||||
|
||||
it 'uses default model when CAPTAIN_HERMES_GATEWAY_MODEL is missing' do
|
||||
InstallationConfig.find_by!(name: 'CAPTAIN_HERMES_GATEWAY_MODEL').delete
|
||||
expect(described_class.settings[:model]).to eq(described_class::DEFAULT_HERMES_GATEWAY_MODEL)
|
||||
end
|
||||
|
||||
it 'uses default URL when CAPTAIN_HERMES_GATEWAY_URL is missing' do
|
||||
InstallationConfig.find_by!(name: 'CAPTAIN_HERMES_GATEWAY_URL').delete
|
||||
expect(described_class.settings[:api_base]).to eq(described_class::DEFAULT_HERMES_GATEWAY_URL)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when default provider (openai_api) is in use' do
|
||||
it 'reports hermes_gateway? as false' do
|
||||
expect(described_class.hermes_gateway?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '.light_model' do
|
||||
it 'returns gpt-4o-mini for openai_api' do
|
||||
expect(described_class.light_model).to eq('gpt-4o-mini')
|
||||
end
|
||||
|
||||
it 'returns DEFAULT_CODEX_MODEL for openai_codex_oauth' do
|
||||
InstallationConfig.create!(name: 'CAPTAIN_LLM_PROVIDER', value: 'openai_codex_oauth')
|
||||
expect(described_class.light_model).to eq(described_class::DEFAULT_CODEX_MODEL)
|
||||
end
|
||||
|
||||
it 'returns DEFAULT_HERMES_GATEWAY_MODEL for openai_hermes_gateway' do
|
||||
InstallationConfig.create!(name: 'CAPTAIN_LLM_PROVIDER', value: 'openai_hermes_gateway')
|
||||
expect(described_class.light_model).to eq(described_class::DEFAULT_HERMES_GATEWAY_MODEL)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user