Por que usar streaming em produção?
Quando um modelo precisa gerar respostas longas, aguardar a resposta completa antes de exibir qualquer caractere causa latência percebida alta. Com streaming, cada trecho ("chunk") é enviado assim que fica pronto, permitindo que a interface reaja em tempo real — como digitação progressiva em um chat.
Em APIs compatíveis com o padrão da OpenAI, para ativar o streaming basta enviar "stream": true no corpo da requisição. A resposta chega como uma sequência de eventos SSE (text/event-stream), com cada linha data: contendo um objeto JSON parcial.
Neste tutorial, você verá como consumir esse stream com JavaScript puro (Node 18+), medir o total de tokens ao final e implementar um tratamento básico para desconexões.
Pré-requisitos
- O endereço da API e uma chave de acesso, fornecidos pela sua equipe de acesso (neste tutorial,
BASE_URLeAPI_KEYem variáveis de ambiente). - Node.js 18 ou superior (para usar
fetchnativo). - Conhecimento básico de promessas e
ReadableStream.
Consumindo o stream chunk a chunk
O primeiro passo é fazer uma requisição POST para /chat/completions com stream: true. No Node, o fetch retorna um Response cujo body é um ReadableStream. Para processar eventos SSE, você lê o corpo em fatias e divide por quebras de linha duplas (\n\n), que separam cada evento.
Exemplo mínimo:
const API_URL = `${process.env.BASE_URL}/chat/completions`;
const API_KEY = process.env.API_KEY;
async function streamChat(messages) {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages,
stream: true
})
});
if (!response.ok) {
throw new Error(`Erro ${response.status}: ${await response.text()}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// Eventos SSE são separados por linha em branco
const parts = buffer.split('\n\n');
buffer = parts.pop() ?? '';
for (const part of parts) {
for (const line of part.split('\n')) {
if (line.startsWith('data:')) {
const data = line.slice(5).trim();
if (data === '[DONE]') continue;
const chunk = JSON.parse(data);
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) {
process.stdout.write(delta); // ou envie para o cliente
}
}
}
}
}
}
Pontos importantes:
- O buffer guarda a parte final que ainda não formou um evento completo.
- O evento
[DONE]indica o fim do stream. - Cada
chunk.choices[0].delta.contentcontém um pedaço do texto. Em modelos que retornam outros campos, o delta pode trazertool_callsoureasoning_content; adapte conforme necessário.
Medindo tokens no fim do stream
Em produção, você frequentemente precisa saber quantos tokens foram consumidos naquela chamada — para controle de uso, telemetria ou rate limiting interno. APIs que seguem o padrão OpenAI podem incluir o campo usage no último chunk se você solicitar explicitamente.
Para isso, adicione stream_options ao corpo da requisição:
{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "Olá"}],
"stream": true,
"stream_options": {"include_usage": true}
}
No loop do stream, trate o chunk final (aquele sem choices ou com usage presente):
let usage = null;
for (const part of parts) {
for (const line of part.split('\n')) {
if (!line.startsWith('data:')) continue;
const data = line.slice(5).trim();
if (data === '[DONE]') continue;
const chunk = JSON.parse(data);
if (chunk.usage) {
usage = chunk.usage; // { prompt_tokens, completion_tokens, total_tokens }
}
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
}
console.log('\nTokens:', usage);
Se por algum motivo o usage não vier no último chunk, você pode estimar localmente com uma biblioteca como tiktoken, mas isso só aproxima o valor e não substitui o dado oficial. A recomendação é sempre usar o stream_options.include_usage.
Lidando com desconexões no meio da resposta
Streams longos podem ser interrompidos por timeouts de rede, reinicialização do servidor ou cancelamento do cliente. Em produção, você precisa pelo menos:
- Detectar o erro.
- Cancelar a requisição ativa para liberar recursos.
- Registrar o ocorrido e decidir se tenta novamente.
O AbortController permite cancelar o fetch do lado do cliente. Você também pode definir um timeout para evitar esperas infinitas:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 120_000); // 2 minutos
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ /* ... */ }),
signal: controller.signal
});
// ... processa o stream
} catch (err) {
if (err.name === 'AbortError') {
console.error('Timeout ou cancelamento do stream.');
} else {
console.error('Falha na conexão:', err);
}
} finally {
clearTimeout(timeout);
}
Ao tentar novamente, cuidado com duplicação: se o modelo já gerou parte da resposta e a conexão caiu, simplesmente reenviar a mesma conversa pode produzir tokens adicionais e inconsistência no histórico. Uma prática comum é não reexecutar automaticamente streams com efeitos colaterais; em chats, pode-se apresentar ao usuário uma mensagem de erro e permitir que ele reenvie. Para casos idempotentes, você pode registrar a mensagem parcial e retomar com um prompt de continuação, mas isso adiciona complexidade.
Outro ponto: use keepalive no fetch se precisar que a requisição sobreviva a um unload da página no navegador, mas lembre-se de que keepalive tem limites de tamanho e não é adequado para streams longos.
Exemplo completo (Node.js)
Juntando as partes:
const API_URL = `${process.env.BASE_URL}/chat/completions`;
const API_KEY = process.env.API_KEY;
async function streamCompletion(messages, { timeoutMs = 120_000 } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let fullContent = '';
let usage = null;
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages,
stream: true,
stream_options: { include_usage: true }
}),
signal: controller.signal
});
if (!response.ok) {
throw new Error(`Erro ${response.status}: ${await response.text()}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split('\n\n');
buffer = events.pop() ?? '';
for (const event of events) {
for (const line of event.split('\n')) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') continue;
const chunk = JSON.parse(payload);
if (chunk.usage) usage = chunk.usage;
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) {
fullContent += delta;
process.stdout.write(delta);
}
}
}
}
return { content: fullContent, usage };
} finally {
clearTimeout(timer);
}
}
// Uso
streamCompletion([{ role: 'user', content: 'Explique stream SSE em 3 frases.' }])
.then(({ content, usage }) => {
console.log('\n---\nTokens usados:', usage);
})
.catch(console.error);
Considerações finais
Streaming por SSE é o padrão de fato para respostas de LLMs em produção. Com uma API compatível com OpenAI, você consegue integrar em poucas linhas e manter controle sobre tokens e falhas de rede.
Teste o fluxo em ambiente de homologação antes de subir, monitore o usage para entender o consumo e trate AbortError e erros HTTP de forma explícita.
Quer entender como a sua empresa pode usar modelos de IA com atendimento e faturamento no Brasil? Fale com a gente — a equipe da TokenRecarga responde em português.