Streaming SSE em uma API compativel
Resposta direta
Para streaming, envie stream: true ao mesmo endpoint Chat Completions e leia os eventos SSE conforme o conteúdo chega; confirme antes o suporte do modelo escolhido.
Como ler eventos SSE de Chat Completions com cURL, Python e JavaScript e verificar a disponibilidade de streaming para o modelo escolhido. O streaming pode mostrar os primeiros trechos antes da resposta completa, mas não garante menor tempo total de geração.
Antes de começar
Mantenha MANDAPI_API_KEY no ambiente do processo. Escolha um ID disponível para sua conta em GET /v1/models ou no seletor do painel e defina MANDAPI_MODEL com esse ID. Um preço publicado não prova que sua chave pode chamar o modelo nem que ele suporta streaming. Confira a capacidade no catálogo atual e teste o modelo escolhido.
Os exemplos JavaScript rodam no servidor com Node.js. Nunca coloque a chave em um bundle do navegador.
Endpoint e modos
Use POST https://api.mandapi.com/v1/chat/completions nos dois modos. Envie "stream": true para receber eventos SSE conforme a resposta é gerada. Envie "stream": false para aguardar a resposta completa em um único JSON. O endpoint é o mesmo.
Exemplo copiável
cURL
Com MANDAPI_API_KEY e MANDAPI_MODEL definidos no ambiente de um shell Bash:
curl -N -sS --fail-with-body https://api.mandapi.com/v1/chat/completions \
-H "Authorization: Bearer ${MANDAPI_API_KEY}" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${MANDAPI_MODEL}\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"Explique a MandAPI em uma frase.\"}]}"
-N desativa o buffer de saída do cURL para mostrar os eventos conforme chegam. Se receber um erro de modelo indisponível ou streaming não suportado, confira o ID e a capacidade para sua chave.
Como ler a resposta SSE
A resposta usa Content-Type: text/event-stream. Cada evento é separado por uma linha vazia e contém uma ou mais linhas data:. Este exemplo simplificado mostra o formato, sem representar uma chamada de produção específica:
data: {"choices":[{"delta":{"content":"Olá"}}]}
data: {"choices":[{"delta":{"content":"!"}}]}
data: [DONE]
Leia um evento por vez: reúna as linhas data:, pare ao receber [DONE], interprete os demais dados como JSON e acrescente cada choices[0].delta.content não vazio ao texto exibido. Alguns eventos contêm metadados sem conteúdo. Não interprete o stream inteiro como um único JSON.
Python com o SDK OpenAI
Este exemplo mostra o formato de uso do SDK com a URL base da MandAPI. A compatibilidade de ponta a ponta do SDK e a disponibilidade de streaming dependem da chave e do modelo escolhidos; o exemplo não atribui status de verificado a outros modelos.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["MANDAPI_API_KEY"],
base_url="https://api.mandapi.com/v1",
)
stream = client.chat.completions.create(
model=os.environ["MANDAPI_MODEL"],
messages=[{"role": "user", "content": "Explique a MandAPI em uma frase."}],
stream=True,
)
for chunk in stream:
if not chunk.choices:
continue
content = chunk.choices[0].delta.content
if content:
print(content, end="", flush=True)
print()
JavaScript com fetch: consumir os eventos
Este exemplo usa Node.js 18 ou superior e lê a resposta HTTP diretamente, sem depender de um SDK. O parser aceita eventos divididos entre leituras e linhas terminadas em LF ou CRLF.
const apiKey = process.env.MANDAPI_API_KEY;
const model = process.env.MANDAPI_MODEL;
if (!apiKey || !model) {
throw new Error("Defina MANDAPI_API_KEY e MANDAPI_MODEL no ambiente.");
}
const response = await fetch("https://api.mandapi.com/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
stream: true,
messages: [{ role: "user", content: "Explique a MandAPI em uma frase." }],
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
if (!response.headers.get("content-type")?.includes("text/event-stream")) {
throw new Error("A resposta não é text/event-stream.");
}
if (!response.body) {
throw new Error("A resposta não tem corpo para leitura.");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finished = false;
try {
while (!finished) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let boundary;
while ((boundary = /\r?\n\r?\n/.exec(buffer)) !== null) {
const event = buffer.slice(0, boundary.index);
buffer = buffer.slice(boundary.index + boundary[0].length);
const data = event.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n");
if (!data) continue;
if (data === "[DONE]") {
finished = true;
break;
}
const chunk = JSON.parse(data);
const content = chunk.choices?.[0]?.delta?.content;
if (content) process.stdout.write(content);
}
}
if (!finished) throw new Error("O stream terminou sem [DONE].");
process.stdout.write("\n");
} finally {
if (finished) await reader.cancel();
reader.releaseLock();
}
