Try the interactive chat
Streaming
How streamed replies arrive, how connections stay alive and why text progresses message by message.
All three formats support streaming: set "stream": true for Chat Completions and Responses; Vercel AI SDK's /chat always streams. All use server-sent events (SSE).
Connection
Streamed response headers:
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no
X-Sone-Conversation-Id: conv_x8k2m9p4q7w1r3tVercel AI SDK streams also include x-vercel-ai-ui-message-stream: v1.
- Checks come first. Key, rate limit, assistant, conversation and plan checks run before streaming starts. A rejected request gets a normal HTTP error: Errors.
- The first event arrives as the assistant starts. After checks pass, Chat Completions sends its first role chunk, Responses sends
response.created, and AI SDK sendsstartimmediately. You know the connection is open and have the conversation ID while the assistant is still thinking. - Heartbeat every ten seconds. Long turns send
: pinglines to keep the connection alive. Lines beginning with a colon are SSE comments. SDKs skip them; your own parser should too. - Completion. Chat Completions and AI SDK end with
data: [DONE]. Responses ends withresponse.completed, orresponse.failedon an error, without[DONE].
Why text arrives message by message
Before a reply reaches the visitor, business safety checks verify the complete message: prices and stock, prohibited promises, and whether it should be sent at all. A message is sent only after it is complete and has passed these checks.
The stream progresses as follows:
- Checks pass, the connection opens and the first event arrives.
- The assistant thinks and uses tools. This can take several seconds. Short activity labels such as “Checking products” arrive during this time.
- The first approved message arrives, quickly streamed in word groups to preserve the typing effect.
- Further messages, product cards and buttons follow in order.
Activity labels appear differently in each format:
| Format | Activity |
|---|---|
| Chat Completions | Top-level sone.activity on a chunk with an empty delta. |
| Responses | An SSE comment, not an event: : sone.activity {"label":"Ürünlere bakılıyor"}. It has no sequence_number. SDKs skip it; parse the stream yourself to read it. |
| Vercel AI SDK | data.label on a transient (transient: true) data-sone-status part, delivered only to onData. |
Activity text is a visitor-friendly Turkish sentence, rather than a tool name. There may be short pauses between messages; keep a typing indicator visible in your UI.
Resending the same message with the same Idempotency-Key, or the conversation ID and matching history, does not generate another reply. The stored reply arrives together at the end of the stream. See Retries.
Disconnections
Each message is saved to the conversation when it is sent. If a stream disconnects, recover missing messages with GET /conversations/{id}/messages. The conversation ID is available at the start in X-Sone-Conversation-Id. See Human handoff and team replies.
If you resend a message, reuse its original Idempotency-Key. The assistant does not answer twice. If the previous turn is still running, the request returns 409 conversation_busy.
Behind a proxy
When forwarding a stream through your server, make sure your proxy does not buffer it. In Nginx, use proxy_buffering off;. In a Next.js route handler, forward the response body unchanged:
export async function POST(request: Request) {
const upstream = await fetch('https://pb.sonecentral.com/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SONE_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': request.headers.get('Idempotency-Key') ?? crypto.randomUUID(),
},
body: await request.text(),
});
return new Response(upstream.body, {
status: upstream.status,
headers: {
'Content-Type': upstream.headers.get('Content-Type') ?? 'text/event-stream',
'Cache-Control': 'no-cache',
'X-Sone-Conversation-Id': upstream.headers.get('X-Sone-Conversation-Id') ?? '',
},
});
}With a server key, your server is responsible for authenticating and rate-limiting visitors. A publishable key is simpler for a website chat widget.