← Artigos · · 5 min de leitura

O Router Vê Tudo: Adicionando Telemetria ao Jano

Um servidor de modelo sabe o próprio tempo da última resposta. Ele não consegue dizer quão profunda está a fila, com que frequência a máquina faz thrashing entre modelos, nem o que todos os outros chamadores andaram recebendo. O router consegue, porque fica acima dos backends e é dono de cada swap. Então eu ensinei ele a fazer isso.

  • local-first
  • llm
  • observability

Jano é o pequeno router que rodo na frente do meu servidor de modelo local. Uma rajada de requests variados custava um swap de modelo cada; o Jano reordena para que a rajada inteira custe um swap só. Escrevi sobre o modelo entre os quais ele troca semana passada. Esta semana dei uma memória para o router.

A razão é uma lacuna que eu vivia esbarrando. Quando eu queria saber como a máquina estava de fato, não tinha o que perguntar. O servidor de modelo consegue dizer ao único cliente que acabou de atender quantos tokens aquela resposta consumiu. Ele não consegue me dizer quantos requests estão esperando, se acabou de gastar dois minutos fazendo thrashing entre modelos, nem como o throughput esteve entre todos os chamadores no último minuto. Essa informação existe. Ela só não está em lugar nenhum que eu consiga alcançar.

Por que o router é o lugar certo para olhar

O Ollama, o stack local padrão por bons motivos, expõe o timing por resposta e qual modelo está carregado. Isso é genuinamente útil, e também é a lista inteira. Não há endpoint para profundidade de fila, nenhum registro de quantas vezes ele trocou, nenhum agregado do que cada chamador andou recebendo. Não porque os autores esqueceram, mas porque um único servidor de modelo é a altitude errada para enxergar essas coisas. Ele atende um request por vez e não tem noção dos outros.

Um router tem. O Jano fica acima dos backends. Todo request passa por ele, toda resposta volta em stream por ele, e é ele que decide quando trocar de modelo e chama o script que faz a troca. Essa posição é o ponto inteiro. Profundidade de fila é um número que ele já tem. A economia de swaps são eventos que ele já dispara. O throughput agregado é o stream que ele já encaminha. Nada disso precisa de um sidecar lendo logs nem de um segundo processo raspando coisa alguma. São contadores e pequenos ring buffers em caminhos de código que já rodam.

Então a cunha é simples: o router consegue reportar nativamente exatamente as coisas que um backend estruturalmente não consegue.

O que ele reporta agora

Três endpoints, todos aditivos em cima dos /health e /status que já existiam.

O GET /status foi o que mais cresceu. Ele já sabia o modelo atual e a profundidade da fila. Agora também carrega quantos requests estão em execução, há quanto tempo o mais antigo está esperando, quando o modelo atual foi carregado e quanto durou o último swap, quantos swaps aconteceram na última hora (o detector de thrashing), os tokens por segundo em tempo real entre todos os chamadores, e contadores cumulativos desde o início. Uma visão reduzida, tirada da minha máquina de verdade, instantes depois de um request real:

{
  "currentModel": "qwen-chat",
  "queueDepth": 0,
  "inFlight": 0,
  "lastSwapDurationMs": null,
  "swapsLastHour": 0,
  "recentGenTokS": 66.3,
  "requestsServedTotal": 1,
  "tokensGeneratedTotal": 48,
  "backendHealth": { "qwen-chat": "up" }
}

Esse 66.3 é a velocidade de geração real do modelo sobre o qual escrevi semana passada, reportada pelo router, não medida por mim com um cronômetro.

O GET /metrics fala o formato texto do Prometheus, então os contadores e um histograma de duração de swap caem direto no Grafana sem tradução. A galera de homelab já raspa esse formato; agora o Jano é mais uma coisa que ela consegue raspar.

O GET /usage?limit=N é um ring dos últimos N requests, cada um com suas contagens de tokens e tempos. Ele responde à pergunta que todo entusiasta de LLM local acaba fazendo: quanto meus requests custaram de verdade em computação?

A única parte que deu trabalho

Os valores são fáceis de reportar depois que você os tem. Obtê-los sem quebrar nada foi onde o cuidado entrou.

A promessa que define o Jano é ser transparente. Ele encaminha o corpo do seu request intocado e devolve a resposta em stream direto, então falar com o router é idêntico a falar com o backend. A telemetria não pode mudar isso. Eu não posso bufferizar a resposta inteira para contar tokens, porque isso quebraria o streaming e prenderia memória em gerações longas.

A solução é grampear o stream conforme ele passa, em vez de segurá-lo. Numa resposta em stream os totais de tokens ficam no último chunk, então uma pequena janela de cauda os captura enquanto os bytes voam para o cliente sem alteração. Numa resposta de uma vez só, o corpo é pequeno o bastante para ler inteiro. Depois as contagens são extraídas de qualquer formato que o backend fale: o llama.cpp reporta um bloco timings com tokens por segundo já calculados, o formato OpenAI reporta um objeto usage, o Ollama reporta durações cruas em nanossegundos. O Jano normaliza os três num número só. Quando um backend não reporta nada utilizável, o campo é nulo. O router nunca inventa um throughput que ele não observou de fato.

A linha que eu não cruzo

Uma coisa que deixei de fora de propósito: hardware. Temperatura de GPU, velocidade de ventoinha, consumo de energia, um router não enxerga nada disso, e um número que você não consegue medir é pior do que um que você não reporta. Isso pertence a um helper no host; o Jano reporta o caminho de inferência que é dele, e nada que ele teria que fingir.

O que isso significa

É o dividendo do local-first aparecendo de novo, numa forma mais quieta do que velocidade pura. A máquina que você tem consegue dizer o que está fazendo, em detalhe, de graça, se você colocar o observador na camada que de fato enxerga. Sem cobrança por request para reconstruir a partir de uma fatura, sem painel de fornecedor mostrando a versão deles do seu uso, sem agente para instalar. Os dados sempre estiveram ali no stream de requests. Só faltava algo parado no lugar certo para manter a conta.

O router já era o melhor lugar da casa. Ele só não estava anotando. Agora está.