iachat/docs/captain-codex-oauth.md
Rodribm10 b457e84c2f fix(captain): route embeddings to legacy OpenAI + retry transient errors
Resolve duas camadas de problema identificadas em teste end-to-end:

1. Embeddings falhavam com HTTP 404 (/codex/v1/embeddings não existe).
   Solução: Captain::Llm::EmbeddingService sempre usa OpenAI tradicional
   via Llm::Config.with_api_key(legacy_settings). ProviderConfig expõe
   legacy_openai_settings pra isso.

2. Servidor Codex ocasionalmente responde com response.failed +
   code=server_error (instabilidade transitória). Client agora retenta
   até 2x com backoff exponencial (0.5s, 1.5s) em erros retryable:
   HTTP 5xx, server_error no response.failed, ou stream inacabado.

Outras correções nesta etapa:
- Scenario#agent_model: em modo Codex, ignora CAPTAIN_OPEN_AI_MODEL_SCENARIO
  (que pode ter gpt-4o legado) e usa ProviderConfig.model.
- ExtractionService/ContradictionCheckerService/TranslateQueryService:
  trocam constantes hardcoded gpt-4o-mini/gpt-4.1-nano por
  ProviderConfig.light_model (respeitando o provider ativo).
- ProviderConfig.DEFAULT_CODEX_MODEL agora é gpt-5.2 (reconhecido pelo
  RubyLLM; gpt-5.4 não está no catalog do gem).

Validado ponta-a-ponta: WhatsApp → Chatwoot → Jasmine → handoff Daniela
→ faq_lookup com embedding OK → resposta com preços corretos.

Docs em docs/captain-codex-oauth.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:42:31 -03:00

8.5 KiB

Captain AI via OAuth ChatGPT Plus (Codex)

Documentação do caminho ponta-a-ponta pra fazer o Captain AI rodar usando a assinatura ChatGPT Plus em vez de API key OpenAI paga por token.

Status: funcional em dev (2026-04-22). Pendente: rollout em staging/prod.


Arquitetura

Captain (RubyLLM / Agents gem / ruby-openai)
         │
         │  POST /v1/chat/completions  (formato OpenAI Chat Completions)
         ▼
┌──────────────────────────────────────────────────────┐
│  Api::Internal::CodexProxyController                 │
│    • traduz chat→responses                           │
│    • Captain::Codex::AuthService.valid_access_token  │  (OAuth refresh automático)
│    • streaming SSE → agregado                        │
│    • retry em erros transitórios                     │
│    • traduz responses→chat                           │
└──────────────────────────────────────────────────────┘
         │
         │  POST https://chatgpt.com/backend-api/codex/responses
         │  Authorization: Bearer <OAuth token>
         ▼
    OpenAI Codex (consome assinatura ChatGPT Plus, sem cobrar por token)

Embeddings NÃO passam pelo proxy. O endpoint Codex não expõe /embeddings, então Captain::Llm::EmbeddingService força o uso da OpenAI API tradicional (requer CAPTAIN_OPEN_AI_API_KEY válida mesmo em modo Codex).

Files API NÃO passa pelo proxy. Mesmo motivo — Llm::LegacyBaseOpenAiService (usado em PdfProcessingService e PaginatedFaqGeneratorService) continua apontando pra OpenAI tradicional.


Componentes

Componente Papel
Captain::CodexCredential (model) Tabela singleton com access_token + refresh_token (AR encrypted)
Captain::Codex::AuthService Device flow OAuth + refresh automático
Captain::Codex::Client HTTP client streaming SSE, com retry em server_error
Captain::Codex::Translator Chat Completions ↔ Responses API (bidirectional)
Api::Internal::CodexProxyController POST /codex/v1/chat/completions
Captain::Llm::ProviderConfig Single source of truth de provider/model/api_base
Captain::Codex::RefreshTokensJob Sidekiq cron: refresh proativo de tokens (30min)
rake captain:codex:{login,status,refresh} Utilitários de ops

Setup em dev

1. Migration

bundle exec rails db:migrate

2. Login OAuth (device flow)

bundle exec rails captain:codex:login

Abre URL no browser → loga com conta ChatGPT Plus → cola código → tokens salvos em captain_codex_credentials.

3. Ativar o provider + configurar modelo

# via rails runner ou bundle exec rails c
InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_LLM_PROVIDER').update!(
  value: 'openai_codex_oauth', locked: false
)
InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_CODEX_PROXY_URL').update!(
  value: 'http://localhost:3000/codex', locked: false
)
InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_MODEL').update!(value: 'gpt-5.2')
InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_API_KEY').update!(value: 'sk-<KEY_VALIDA>')

Importante sobre CAPTAIN_OPEN_AI_API_KEY: mesmo em modo Codex OAuth, a key precisa ser válida — é usada apenas pra embeddings (/embeddings não existe no Codex) e file uploads. Sem essa key, faq_lookup e memory recall falham.

4. Reinicia Rails

pkill -9 -f 'overmind|vite|sidekiq|rails' 2>/dev/null; sleep 3
rm -f ./.overmind.sock && pnpm run dev

5. Teste direto no proxy

curl -X POST http://localhost:3000/codex/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.2","messages":[{"role":"user","content":"Diga: OK"}]}'

Espera: JSON no formato OpenAI Chat Completions com choices[0].message.content.


Modelos suportados

O endpoint Codex via ChatGPT Plus aceita os modelos da família GPT-5 do Hermes:

Modelo Uso RubyLLM reconhece?
gpt-5.2 Default atual — conversação
gpt-5.1 Fallback conversacional
gpt-5-codex, gpt-5.1-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini Code-focused
gpt-5.4, gpt-5.3-codex Mais novos, melhor qualidade ✗ (não no catalog do gem)
gpt-4o, gpt-4o-mini NÃO funciona no endpoint Codex

Pra usar gpt-5.4/gpt-5.3-codex no futuro: adicionar sobrescrita no proxy que mapeia modelo recebido → modelo enviado ao Codex (evita validação do RubyLLM).


Peculiaridades da Responses API (vs Chat Completions)

O Translator lida com as seguintes diferenças:

Campo Chat Completions Responses
Path /chat/completions /responses
Mensagens messages: [] input: []
System prompt {role: "system", content: "..."} instructions: "..." (top-level, obrigatório)
Tools wrapper {type: "function", function: {name, description, parameters}} {type: "function", name, description, parameters, strict}
Tool result {role: "tool", tool_call_id, content} {type: "function_call_output", call_id, output}
Assistant tool_call {role: "assistant", tool_calls: [...]} {type: "function_call", call_id, name, arguments}
Streaming Opcional (stream: true) Obrigatório
temperature/top_p Aceitos Rejeitados (modelos reasoning)
max_tokens max_tokens max_output_tokens
Output final choices[].message output: [items] via SSE events
Storage Default persiste store: false obrigatório

Troubleshooting

Erro: "Stream must be set to true"

Request enviou stream: false. O Translator força stream: true — verifique se não há caminho que bypassa o Translator.

Erro: "The '<model>' model is not supported when using Codex with a ChatGPT account."

Algum service está com modelo hardcoded inaceitável (gpt-4o, gpt-4o-mini). Verifique CAPTAIN_OPEN_AI_MODEL e CAPTAIN_OPEN_AI_MODEL_SCENARIO.

Erro: "RubyLLM::ModelNotFoundError: Unknown model: <model>"

O modelo não está no catalog do RubyLLM. Use gpt-5.2 ou gpt-5.1 (lista atual em RubyLLM.models.all.map(&:id)).

Erro: "Incorrect API key provided" em embedding

CAPTAIN_OPEN_AI_API_KEY inválida. Embeddings sempre usam OpenAI tradicional, mesmo em Codex OAuth.

Erro: "response.failed" com code=server_error

Instabilidade do endpoint Codex ou rate limit da assinatura Plus. O Client já retenta 2x com backoff (0.5s, 1.5s). Se persistir, pode ser sinal de que precisa subir de plano (Team/Pro).

Voltar pra API tradicional (rollback rápido)

InstallationConfig.find_by!(name: 'CAPTAIN_LLM_PROVIDER').update!(value: 'openai_api')
InstallationConfig.find_by!(name: 'CAPTAIN_OPEN_AI_MODEL').update!(value: 'gpt-4o-mini')

Depois restart.


Ordem de commits (historical)

Branch: feat/captain-codex-oauth

  1. chore(captain): PoC Codex OAuth device flow + Responses streaming — PoC standalone em Ruby puro (scripts/captain_codex_poc/)

  2. feat(captain): Codex OAuth auth module + proxy controller — Migration, AuthService, Translator, Client, Controller

  3. fix(captain): always include instructions in Codex responses body — Codex exige instructions mesmo quando não tem system message

  4. feat(captain): feature flag CAPTAIN_LLM_PROVIDER + ProviderConfig central — Toggle openai_api vs openai_codex_oauth

  5. fix(captain): route embeddings to legacy OpenAI + retry transient errors — Embeddings via OpenAI tradicional + retry automático no Client


Riscos conhecidos

  • ToS: uso comercial da assinatura ChatGPT Plus via OAuth não-oficial viola os termos da OpenAI. OpenAI pode cortar a conta sem aviso, derrubando todos os hotéis ao mesmo tempo.
  • Rate limits não documentados: ChatGPT Plus tem limites de mensagens/hora que não são públicos. Pode bater limite em horário de pico.
  • Client_id do Hermes: reusamos app_EMoamEEZ73f0CkXaXp7hrann. Se o Hermes regerar o app ou a OpenAI bloquear por terceiros, quebra.
  • Modelos Codex: otimizados pra código. Qualidade conversacional pode ser inferior ao gpt-4o em alguns cenários.
  • Fallback não automático: se o Codex falhar persistentemente, alternância pra openai_api é manual. Rollout em prod deve considerar automação.