Skip to main content
With stream: true enabled, OpenPAI pushes chunks as Server-Sent Events (SSE), so the client can render as it receives.

Protocol format

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"He"}}]}

data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"llo"}}]}

data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"stop"}]}

data: [DONE]
Notes:
  • Each chunk starts with data: and ends with two \n.
  • A final data: [DONE] is sent to signal the end (the Anthropic / Gemini protocols have no such convention).
  • The gateway sends a :keep-alive comment line as a heartbeat every 15 seconds; the client doesn’t need to handle it.

Python (openai SDK)

from openai import OpenAI
client = OpenAI(api_key="sk-...", base_url="https://openp.ai/v1")

stream = client.chat.completions.create(
    model="gpt-5.5",
    messages=[{"role": "user", "content": "Tell a story"}],
    stream=True,
    stream_options={"include_usage": True},
)

for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\nUsage:", chunk.usage)

Node.js

import OpenAI from "openai";
const client = new OpenAI({ apiKey: "sk-...", baseURL: "https://openp.ai/v1" });

const stream = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [{ role: "user", content: "Tell a story" }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Browser / Fetch API

const resp = await fetch("https://openp.ai/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "gpt-5.5",
    messages: [{ role: "user", content: "hi" }],
    stream: true,
  }),
});

const reader = resp.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 });

  let idx;
  while ((idx = buffer.indexOf("\n\n")) !== -1) {
    const line = buffer.slice(0, idx).trim();
    buffer = buffer.slice(idx + 2);
    if (!line.startsWith("data:")) continue;
    const payload = line.slice(5).trim();
    if (payload === "[DONE]") return;
    const json = JSON.parse(payload);
    console.log(json.choices[0]?.delta?.content);
  }
}
Connecting directly from the browser exposes your API key; in production, forward through your own backend.

Errors during streaming

If the upstream errors mid-stream, it’s sent as SSE:
data: {"error":{"message":"Upstream timeout","type":"server_error"}}
data: [DONE]
The client must check the error field, not just the HTTP status code (the streaming response already returned 200 early).

include_usage

With stream_options.include_usage: true, an extra chunk with no content but with usage is sent at the end of the stream, for accounting:
{"choices":[],"usage":{"prompt_tokens":10,"completion_tokens":42,"total_tokens":52}}

Claude / Gemini streaming

  • Claude: events come in the order message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop; see Messages.
  • Gemini: with ?alt=sse, chunks are pushed as data: {...} with no [DONE] terminator — detect the end via finishReason.

Notes

  • Timeouts: the OpenPAI gateway defaults to 600 seconds; for long outputs ensure your client timeout is long enough too.
  • Cancel mid-stream: after the client disconnects, the gateway immediately stops the upstream request and settles by tokens already generated.
  • Proxy / CDN: every intermediate hop must support chunked transfer, or SSE will be buffered.