Coloquei uma chave falsa da OpenAI no ambiente, apontei o llm pra um endpoint de mentira em 127.0.0.1 e fiquei olhando o cabeçalho Authorization chegar. No llm 0.37.1, a chave da OpenAI apareceu no endpoint de terceiro em 3 de 5 cenários. Os três terminaram com código de saída 0 e a resposta normal na tela. No 0.37.2, saído em 11/11/2026, foram 0 de 5.
O código está no repositório: demos/14-llm-chave-endpoint. Roda em CPU, sem conta e sem chave de verdade.
O que é o llm e por que isso importa?
O llm é a ferramenta de linha de comando e biblioteca Python do Simon Willison pra conversar com modelos. Licença Apache-2.0. Muita gente usa ele com endpoint próprio: vLLM, LM Studio, OpenRouter, o gateway da empresa. Isso se faz num arquivo extra-openai-models.yaml, com um api_base apontando pra fora da OpenAI.
Segundo os avisos, a documentação diz que, com api_base definido, a chave da OpenAI que você configurou não vai por padrão. Dois avisos de segurança publicados em 11/11/2026 mostram dois caminhos em que ela ia:
- GHSA-v9h7-jj4g-f862: modelo com
api_baseeapi_key_name, quando a chave com esse nome nunca foi guardada. Ollmcaía noOPENAI_API_KEYdo ambiente e mandava ela pro endpoint. Afeta da 0.6 até a 0.37.1. - GHSA-r639-rr7r-x72w: modelo com
responses: trueeapi_base, rodado com-o chat_completions 1. O objeto que faz a chamada de Chat Completions não herdava a configuração de chave e usava a da OpenAI. Afeta da 0.32a2 até a 0.37.1.
Os dois são classificados como moderados. Ler o aviso é uma coisa. Eu queria ver o cabeçalho chegando.
Como eu rodei?
Montei um endpoint falso compatível com OpenAI, em Python puro, ouvindo só em 127.0.0.1 numa porta aleatória. Ele guarda o Authorization de cada pedido e devolve um chat.completion mínimo com “ok”.
def do_POST(self):
tamanho = int(self.headers.get("Content-Length") or 0)
corpo = self.rfile.read(tamanho) if tamanho else b""
self.server.pedidos.append({
"path": self.path,
"authorization": self.headers.get("Authorization"),
"model": (json.loads(corpo or b"{}") or {}).get("model"),
})
# ... responde um chat.completion com "ok"
O demo.py cria dois venvs, um com llm==0.37.1 e outro com llm==0.37.2. Pra cada cenário ele monta um LLM_USER_PATH temporário com o YAML e, quando o cenário pede, um keys.json. A chave canário é sk-CANARIO-OPENAI-FALSA-0000. Depois roda o mesmo comando nas duas versões:
llm -m tp --no-stream --no-log oi
O YAML do cenário A é este:
- model_id: tp
model_name: "modelo-falso"
api_base: "http://127.0.0.1:PORTA/v1"
api_key_name: "terceiro"
Os cinco cenários:
- A:
api_key_name: terceiroque nunca foi guardada,OPENAI_API_KEYno ambiente. - B (controle): a mesma coisa, mas com a chave do terceiro guardada.
- C (controle):
api_basesemapi_key_name. - D:
responses: truecom-o chat_completions 1,OPENAI_API_KEYno ambiente. - E: igual ao D, mas com a chave
openaiguardada nokeys.json, como faz ollm keys set openai.
Python 3.11. O pip resolveu openai 3.50.0 nos dois venvs. Com os venvs prontos, a demo inteira levou 8,9 s.
O que chegou no endpoint?
| cenário | 0.37.1 recebeu | 0.37.2 recebeu |
|---|---|---|
| A | chave da OpenAI (saída 0) | nenhum pedido (saída 1) |
| B (controle) | chave do terceiro | chave do terceiro |
| C (controle) | DUMMY_KEY |
DUMMY_KEY |
| D | chave da OpenAI (saída 0) | DUMMY_KEY |
| E | chave da OpenAI (saída 0) | DUMMY_KEY |
| vazou | 3 de 5 | 0 de 5 |
Os controles B e C deram igual nas duas versões. Ou seja, a correção fechou os três buracos sem mudar os dois controles.
No cenário A, o 0.37.2 nem faz o pedido. Ele para antes, com esta mensagem:
Error: No key found - add one using 'llm keys set terceiro'
Isso é o comportamento certo. Você pediu uma chave chamada “terceiro”, ela não existe, então o programa reclama. A versão anterior “resolvia” o problema pegando a chave errada em silêncio.
Por que isso passa batido?
Porque nada dá errado na tela. No 0.37.1 os cinco cenários responderam “ok” com código de saída 0. Se o endpoint de terceiro aceita qualquer chave, como o falso da demo aceita, você nunca percebe que a sua chave da OpenAI saiu da máquina.
Confesso que eu achava que o cenário A era o seguro. Dei um nome pra chave, então a ferramenta ia usar aquele nome. Ela usava o nome quando ele existia. Quando não existia, caía no ambiente.
Pensa num carteiro que, quando não acha o destinatário, entrega a carta pro vizinho em vez de devolver. O endereço estava certo. O plano B é que era o problema.
O que não funcionou na primeira rodada?
A primeira execução travou logo no primeiro cenário e estourou o tempo limite do subprocess. Não era o llm. Era eu: o llm lê a entrada padrão quando ela não é um terminal, e o subprocess deixava a entrada aberta. Ele ficava esperando eu digitar o resto do prompt. Resolvi com stdin=subprocess.DEVNULL.
Antes de achar isso, por precaução, tirei o proxy da jogada com NO_PROXY=127.0.0.1,localhost. A máquina onde rodei passa a internet por um proxy, e eu não queria o pedido pro endpoint local desviado. Deixei no código pra quem for reproduzir dentro de uma rede de empresa.
Até onde esse teste vai?
- Endpoint falso e chave falsa. A demo mostra o que o cliente manda. Não mostra o que um terceiro faria com a chave.
- A demo cobre o caminho da linha de comando com modelo definido em
extra-openai-models.yaml. A API Python, o modo assíncrono, os plugins e o comandollm openai endpoint ... --responses, que o segundo aviso também cita, ficaram fora desta rodada. - São 5 cenários montados a partir dos dois avisos. Não é varredura de todas as combinações de opção.
- Quem só usa a OpenAI direto, sem
api_base, não é afetado por essas duas falhas.
Pra quem serve e o que fazer agora?
Serve pra quem usa o llm com modelo local ou gateway e tem chave da OpenAI no ambiente ou guardada nele. Em máquina de dev isso acontece fácil: a variável OPENAI_API_KEY fica no .bashrc há anos e ninguém lembra.
O que fazer:
pip install -U "llm>=0.37.2"
llm --version
Se você usou api_base de terceiro numa versão anterior, nos cenários A, D ou E, troque a chave da OpenAI. Não dá pra saber pelo lado do cliente se o terceiro guardou o cabeçalho. Na minha humilde opinião, vale também tirar o OPENAI_API_KEY do ambiente global e passar a chave só pro processo que precisa dela.
O desafio de implementar a correção é zero: é atualizar um pacote. O custo real está no inventário. Quem tem o llm em servidor de CI, em imagem Docker ou em script de agente precisa achar todas as cópias, e isso costuma levar mais tempo que o pip install.
Um ponto bom pro mantenedor: os dois avisos têm passo a passo de reprodução, versões afetadas e o teste que prova a correção. Foi isso que tornou a demo possível sem adivinhação.
Fonte e como reproduzir
Release 0.37.2 do llm, de 11/11/2026: github.com/simonw/llm/releases/tag/0.37.2. Avisos: GHSA-v9h7-jj4g-f862 e GHSA-r639-rr7r-x72w.
Código e saída real: demos/14-llm-chave-endpoint. Três comandos:
cd demos/14-llm-chave-endpoint && pip install -r requirements.txt
python3 demo.py
python3 -m pytest tests -q -s
O teste confere que o 0.37.1 vazou exatamente nos cenários A, D e E e que o 0.37.2 não vazou em nenhum. A saída fica em results/saida.txt e results/resultado.json.
Vocês usam o llm ou outra ferramenta com api_base de terceiro? Qual delas querem que eu passe pelo mesmo endpoint falso?
English summary
I ran the same 5 scenarios on llm 0.37.1 and 0.37.2 against a fake OpenAI-compatible endpoint on 127.0.0.1 with a canary key. 0.37.1 sent the OpenAI key to the third-party api_base in 3 of 5 scenarios, all exiting 0; 0.37.2 sent it in none, and stops with “No key found” when the named key is missing. Upgrade to 0.37.2 and rotate the key if you used a custom endpoint before. Code: demos/14-llm-chave-endpoint.
Publicado em 13/11/2026. A demo faz parte do nursia-research-lab, repositório de pesquisa do PPGINFOS/UFSC com bolsa FAPESC.

