When you call a chat-completions endpoint, you have two ways to read
the answer. You can wait for the whole reply and parse one JSON object,
or you can read the reply as it is generated, token by token, over
Server-Sent Events. WebWright's POST /v1/chat/completions supports both, and it takes one
field to switch.
Buffered is the default
Send "stream": false and you get a single JSON body back.
This is the right choice for anything that runs without a human
watching: batch jobs, background enrichment, a summary stored for
later. The code is short and the error handling is simple.
curl https://webwright.ai/v1/chat/completions \
-H "Authorization: Bearer www_v1_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "wren-r1",
"messages": [{"role": "user", "content": "Summarize this ticket in one line."}],
"stream": false
}' Streaming is for anything a person watches
Set "stream": true and the server sends the answer as a
sequence of SSE events. Each event carries a small delta of the
message. Your client appends each delta to a buffer and renders it. The
user sees the first words immediately instead of waiting for the whole
reply. For a chat UI, an autocomplete box, or a live coding assistant,
that perceived speed is the whole point.
The events follow the OpenAI wire format. Each line starts with data:, the payload is a JSON object with choices[0].delta.content, and the server ends the stream
with a final data: [DONE] sentinel. Your loop stops on
that sentinel or on the stream closing.
from openai import OpenAI
client = OpenAI(
base_url="https://webwright.ai/v1",
api_key="www_v1_YOUR_API_KEY",
)
stream = client.chat.completions.create(
model="wren-r1",
messages=[{"role": "user", "content": "Write a haiku about retries."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) Three things that break stream readers
- Splitting on newlines is not enough. A network read can cut an event in half. Buffer the raw text, split on the SSE event boundary, and only parse complete events.
- The first chunk often has an empty delta. Some providers send a role-only chunk first. Guard the empty case instead of assuming every chunk carries text.
- Buffering the whole stream defeats the point. If you collect every delta and print at the end, you have written the buffered path with extra steps. Render as you append.
The decision rule
Stream when a person is waiting and partial output is useful. Buffer when nothing is watching and you want one object to store. The request payload is otherwise the same, so you can start buffered and add streaming later without changing your integration.
If you are evaluating an OpenAI-compatible provider and want to talk through your integration, you can book a short session here.
The WebWright Team