Codex CLI falando com GLM: a ponte que o SIGINT derrubou
Diagnóstico de um alias codex-glm que parou de funcionar: um Ctrl+C matou a ponte Uvicorn junto com o processo. Como funciona um shim de API entre clientes incompatíveis.
O problema
Imagine que eu criei um alias — que nada mais é do que um atalho de comando no terminal — chamado codex-glm. A ideia era simples: rodar o Codex (uma ferramenta de linha de comando da OpenAI que ajuda a programar), mas trocando o modelo de inteligência artificial por trás dele. Em vez de conversar com a OpenAI, eu queria conversar com o modelo GLM da Zhipu.
O detalhe é que o Codex só ententende o formato de conversa (a API) da OpenAI, e a Zhipu fala um “dialeto” diferente. Para que os dois pudessem se entender, era necessária uma ponte (um tradutor) no meio do caminho.
A reclamação que chegou foi: “meu atalho codex-glm está dando problema, dá uma olhada na última sessão”. O sintoma era o pior tipo de bug: funcionava perfeitamente várias vezes e, do nada, parava de funcionar sem emitir nenhuma mensagem clara. Minha primeira tentativa de diagnóstico foi rodar o atalho e ver o que acontecia. Foi aí que apareceu a primeira pista.
O instrumento de diagnóstico: um mock TTY
Quando rodamos o Codex em um terminal normal, não dá para ver o que acontece por baixo dos panos porque ele exige um terminal interativo (chamado de TTY, que é a tela do terminal esperando ações do usuário).
Para conseguir capturar o que o programa estava fazendo sem travar a tela, a técnica foi rodar o comando com TMUX="mock". Essa configuração simula uma tela de terminal (um mock TTY), forçando o Codex a operar em modo não-interativo (sem esperar que alguém digite nada na tela):
TMUX="mock" codex-glm "responda apenas 'teste ok'"
A saída veio limpa:
› responda apenas 'teste ok'
• teste ok
Ou seja, o modelo respondeu. A ponte tradutora funcionava e o atalho funcionava. Então por que tinha parado antes? A pista estava no que aconteceu quando a execução terminou — e na forma como a ponte tinha sido iniciada.
A anatomia do alias: por que precisa de ponte
Para entender o bug, é preciso entender o que o atalho faz por dentro.
O Codex espera enviar dados para um endereço específico (o endpoint https://api.openai.com/v1/responses). Já a Zhipu responde em outro endereço (/v1/chat/completions, o formato de chat tradicional). Não dá para apenas trocar a URL nas configurações, porque os dois protocolos são incompatíveis: um diz “me dá uma resposta” e o outro diz “continue esta conversa”.
A solução clássica é criar uma ponte: um pequeno servidor Web (servidor HTTP) que roda localmente no próprio computador. Ele finge ser a OpenAI para o Codex, recebe o pedido, traduz para o formato da Zhipu, envia, recebe a resposta e traduz de volta. Em Python, normalmente isso é feito com uma ferramenta chamada Uvicorn servindo uma aplicação FastAPI/Starlette em uma porta local (neste caso, a porta 4022).
O atalho fazia o seguinte processo:
alias codex-glm='uvicorn responses_bridge:app --port 4022 & \
OPENAI_BASE_URL=http://localhost:4022/v1 codex'
Ele lança a ponte em segundo plano (usando o &), aponta o Codex para o endereço local dessa ponte (http://localhost:4022/v1) e deixa o Codex rodar. Quando o Codex termina, a ponte continua lá em segundo plano, pronta para a próxima chamada. Ao menos, esse era o plano.
O bug: SIGINT mata o grupo de processos inteiro
O problema apareceu quando o Codex estava gerando uma resposta longa e eu apertei Ctrl+C no teclado para cancelar a execução.
O nosso instinto é pensar que o Ctrl+C cancela apenas o Codex. Mas o que o sistema operacional realmente faz é enviar um sinal de interrupção chamado SIGINT para todo o grupo de processos associado àquele terminal. Isso inclui o programa principal e todos os seus “filhos”, inclusive a ponte Uvicorn rodando em segundo plano.
O Uvicorn recebe o SIGINT e faz um encerramento controlado (Shutting down...). Ou seja: no momento em que cancelei uma geração de texto, matei junto a ponte que iria atender a próxima chamada. Como a ponte não reiniciava sozinha, na próxima vez que rodei codex-glm, o atalho tentou conectar o Codex na porta localhost:4022 — onde não havia mais nada escutando. O resultado foi uma conexão recusada e a impressão de que o atalho “parou de funcionar”.
O culpado não era nem o Codex, nem a Zhipu, nem o modelo GLM. Era o fato de duas coisas estarem presas ao mesmo grupo de processos, onde um sinal enviado para parar uma acabou matando a outra.
As três correções possíveis
A primeira correção, a mais simples e rústica, é relançar a ponte a cada chamada do comando. Em vez de usar & (deixar rodando solta em segundo plano), o atalho liga a ponte, espera ela carregar, roda o Codex e encerra a ponte no final:
codex-glm() {
uvicorn responses_bridge:app --port 4022 &
local pid=$!
sleep 1 # espera a ponte subir
OPENAI_BASE_URL=http://localhost:4022/v1 codex "$@"
kill $pid 2>/dev/null
}
Funciona, mas a ponte demora cerca de 1 segundo para subir a cada chamada e, se o Codex travar, o comando kill que fecharia a ponte não é executado.
A segunda opção, mais robusta, é desacoplar a ponte do terminal onde roda o atalho. Em vez de iniciar junto com o comando, a ponte é lançada como um serviço do próprio sistema (como um serviço de usuário do systemd ou dentro de um gerenciador de sessões como tmux ou screen). Dessa forma, o grupo de processos do terminal contém apenas o Codex. Ao apertar Ctrl+C, você cancela apenas o Codex e nada mais:
# ponte roda como serviço, sempre disponível
systemctl --user start codex-glm-bridge
alias codex-glm='OPENAI_BASE_URL=http://localhost:4022/v1 codex'
Essa é a forma mais limpa. A ponte vira parte da infraestrutura do sistema e o atalho vira apenas uma configuração de variável de ambiente.
A terceira opção, que foi a que efetivamente usamos para resolver o problema na sessão, é rodar a ponte usando os comandos nohup e setsid, que a desligam completamente do grupo de processos do terminal:
setsid uvicorn responses_bridge:app --port 4022 </dev/null >/tmp/bridge.log 2>&1 &
O comando setsid cria uma nova sessão para o processo, garantindo que o sinal SIGINT do Ctrl+C no terminal não alcance a ponte. O nohup faz com que ela ignore o sinal de fechamento de terminal (SIGHUP). Já o redirecionamento das entradas e saídas (</dev/null >/tmp/bridge.log 2>&1) desfaz qualquer vínculo com o terminal. A ponte continua viva e independente do que acontecer com o Codex.
O que aprendi
A lição mais valiosa é sobre como funcionam os grupos de processos e terminais no Linux/Unix (POSIX). Quem está acostumado a usar o & para mandar tarefas para o segundo plano geralmente não percebe que o processo “filho” continua no mesmo grupo, e que um Ctrl+C no terminal vai atingir todo mundo. Isso não é um erro do programa, é a forma como o sistema operacional foi projetado. Para separar um processo de verdade, é preciso usar setsid ou um gerenciador externo.
A segunda lição é que o diagnóstico de atalhos de shell precisa de um terminal simulado (mock TTY). Muitas ferramentas modernas de linha de comando (como Codex, Claude ou Aider) mudam de comportamento ou ficam inativas quando não detectam uma tela interativa real. Rodar com TMUX="mock" ou em uma tela simulada é o que permite visualizar o que realmente acontece. Sem isso, você fica tentando resolver um problema invisível.
E a terceira: uma ponte de API nunca deveria depender da execução de um atalho no shell. Pontes são infraestrutura. Se duas ferramentas incompatíveis precisam conversar, o tradutor deve ser um serviço estável — mantido por ferramentas como systemd, supervisor ou tmux — e não um & colocado no final de um atalho. Quando a ponte é estável, o atalho vira algo muito simples: basta apontar a variável de ambiente para o endereço localhost correto.
Comentários
Carregando comentários…