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();
}

← Voltar para API compativel com OpenAI