Pular para o conteúdo

Streaming SSE

Use stream: true para receber text/event-stream. O projeto precisa ter permissão de streaming. O exemplo usa cURL e jq; -N desativa o buffer de saída.

Janela do terminal
jq -n --arg model "$NEXVRA_MODEL" \
'{model: $model, messages: [{role: "user", content: "Olá!"}], max_tokens: 128, stream: true, stream_options: {include_usage: true}}' |
curl --silent --show-error --fail-with-body -N --connect-timeout 10 --max-time 60 \
"$NEXVRA_BASE_URL/chat/completions" \
-H "Authorization: Bearer $NEXVRA_API_KEY" \
-H 'Content-Type: application/json' --data-binary @-
  1. Confira o status HTTP e o Content-Type antes de ler o stream.
  2. Leia eventos SSE separados por uma linha em branco. Chunks de rede não correspondem necessariamente a eventos; mantenha um buffer entre leituras.
  3. Decodifique o JSON do campo data e concatene choices[].delta.content quando presente. Nem todo evento contém texto.
  4. Se houver error, trate a resposta como falha mesmo com HTTP 200.
  5. Reconheça data: [DONE] como encerramento do protocolo de sucesso. Uma conexão encerrada sem conclusão válida não prova sucesso.

finish_reason pode indicar length ou content_filter; o fim do protocolo não significa necessariamente que o texto terminou da forma esperada pelo usuário.

Quando solicitado e conhecido, o uso pode chegar em evento separado com choices: []. Uso ausente/desconhecido não representa zero tokens.

Após o início do SSE, o servidor não pode mudar o status HTTP já enviado. Pode emitir um envelope de erro e encerrar sem [DONE]. Cancelamento ou falha de rede também pode interromper a conexão. Não reinicie automaticamente uma geração que já entregou texto parcial.

O comando cURL mostra os eventos brutos. Seu exit code, isoladamente, não valida erros enviados dentro de um stream com HTTP 200.

Requer Python 3 e as três variáveis dos primeiros passos. O exemplo usa a biblioteca padrão, combina linhas data, aceita eventos sem texto, trata o envelope de erro e falha quando a conexão termina sem [DONE]. O timeout de leitura de 60 segundos não é uma garantia de prazo total do stream.

stream.py
"""Python 3: consome eventos SSE completos e exige [DONE] para concluir."""
import json
import os
import sys
from urllib.error import HTTPError
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
body = json.dumps({
"model": os.environ["NEXVRA_MODEL"],
"messages": [{"role": "user", "content": "Olá! Responda em uma frase."}],
"max_tokens": 128,
"stream": True,
"stream_options": {"include_usage": True},
}).encode()
request = Request(os.environ["NEXVRA_BASE_URL"].rstrip("/") + "/chat/completions", data=body, headers={
"Authorization": "Bearer " + os.environ["NEXVRA_API_KEY"],
"Content-Type": "application/json",
})
try:
with build_opener(NoRedirect).open(request, timeout=60) as response:
if response.headers.get_content_type() != "text/event-stream":
raise ValueError("Resposta não é text/event-stream")
data_lines = []
done = False
for raw_line in response:
line = raw_line.decode("utf-8").rstrip("\r\n")
if line.startswith("data:"):
value = line[5:]
data_lines.append(value[1:] if value.startswith(" ") else value)
elif line == "" and data_lines:
data = "\n".join(data_lines)
data_lines = []
if data == "[DONE]":
done = True
break
event = json.loads(data)
if "error" in event:
raise ValueError("Falha SSE: " + json.dumps(event["error"]))
for choice in event.get("choices", []):
text = choice.get("delta", {}).get("content")
if text:
print(text, end="", flush=True)
if not done:
raise ValueError("Stream encerrado sem [DONE]; resposta incompleta")
print()
except HTTPError as error:
print(f"HTTP {error.code}; request-id={error.headers.get('x-nexvra-request-id', '')}", file=sys.stderr)
print(error.read().decode("utf-8", errors="replace"), file=sys.stderr)
sys.exit(1)
except (ValueError, OSError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)

Execute com python3 stream.py. Se a geração falhar depois de entregar texto, o texto parcial permanece impresso e o exit code indica falha.