Implementa Fases 1+2 do plano Captain Codex OAuth.
Fase 1 — Auth módulo:
- Migration captain_codex_credentials (tokens AR-encrypted)
- Model Captain::CodexCredential (singleton-ish com .current)
- Captain::Codex::AuthService com device flow completo:
start_device_login, poll_once, exchange_for_credential,
valid_access_token (auto-refresh), refresh!
- Rake task captain:codex:{login,status,refresh}
- Sidekiq job Captain::Codex::RefreshTokensJob rodando a cada 30min
Fase 2 — Proxy Chat Completions → Responses:
- Captain::Codex::Translator (chat ↔ responses, tools, tool_calls)
- Captain::Codex::Client (streaming SSE → agregado)
- Api::Internal::CodexProxyController expondo
POST /codex/v1/chat/completions
- 10 specs do Translator passando
Próximo: Fase 3 (feature flag + fallback) e reconfiguração dos
clientes RubyLLM/Agents/ruby-openai pra apontarem pro proxy quando
CAPTAIN_LLM_PROVIDER=openai_codex_oauth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.1 KiB
Ruby
58 lines
2.1 KiB
Ruby
# Proxy interno que traduz OpenAI Chat Completions ↔ OpenAI Responses (Codex).
|
|
#
|
|
# Recebe requests no formato Chat Completions (o que RubyLLM, Agents gem e
|
|
# ruby-openai geram) e encaminha para a Responses API do ChatGPT Plus (Codex)
|
|
# usando OAuth interno via Captain::Codex::AuthService.
|
|
#
|
|
# Rota: POST /codex/v1/chat/completions
|
|
#
|
|
# Acesso: interno (não autenticado — localhost-only via Docker network).
|
|
# Em produção, o Nginx NÃO expõe /codex/* publicamente.
|
|
class Api::Internal::CodexProxyController < ApplicationController
|
|
skip_before_action :verify_authenticity_token, raise: false
|
|
|
|
def chat_completions
|
|
chat_body = request.request_parameters.presence || parse_body
|
|
return render_error('Empty request body', status: 400) if chat_body.blank?
|
|
|
|
render json: proxy_call(chat_body)
|
|
rescue Captain::Codex::AuthService::AuthError, Captain::Codex::Client::Error, StandardError => e
|
|
handle_proxy_error(e)
|
|
end
|
|
|
|
private
|
|
|
|
def handle_proxy_error(error)
|
|
case error
|
|
when Captain::Codex::AuthService::AuthError
|
|
Rails.logger.error("[Codex Proxy] Auth error: #{error.message}")
|
|
render_error("Codex auth error: #{error.message}", status: 401)
|
|
when Captain::Codex::Client::Error
|
|
Rails.logger.error("[Codex Proxy] Upstream error: #{error.message}")
|
|
render_error("Upstream error: #{error.message}", status: error.http_status || 502)
|
|
else
|
|
Rails.logger.error("[Codex Proxy] Unexpected: #{error.class} #{error.message}\n#{error.backtrace.first(5).join("\n")}")
|
|
render_error("Internal error: #{error.message}", status: 500)
|
|
end
|
|
end
|
|
|
|
def proxy_call(chat_body)
|
|
responses_body = Captain::Codex::Translator.chat_to_responses(chat_body)
|
|
aggregated = Captain::Codex::Client.new.responses(responses_body)
|
|
Captain::Codex::Translator.responses_to_chat(aggregated)
|
|
end
|
|
|
|
def parse_body
|
|
raw = request.raw_post
|
|
return {} if raw.blank?
|
|
|
|
JSON.parse(raw)
|
|
rescue JSON::ParserError
|
|
{}
|
|
end
|
|
|
|
def render_error(message, status:)
|
|
render json: { error: { message: message, type: 'codex_proxy_error' } }, status: status
|
|
end
|
|
end
|