# Streaming

Page: https://docs.sonecentral.com/en/akis
Markdown: https://docs.sonecentral.com/en/akis.md

> 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:

```http
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_x8k2m9p4q7w1r3t
```

Vercel 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](https://docs.sonecentral.com/en/hatalar.md).
- **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 sends `start` immediately. You know the connection is open and have the conversation ID while the assistant is still thinking.
- **Heartbeat every ten seconds.** Long turns send `: ping` lines 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 with `response.completed`, or `response.failed` on 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:

1. Checks pass, the connection opens and the first event arrives.
2. The assistant thinks and uses tools. This can take several seconds. Short activity labels such as “Checking products” arrive during this time.
3. The first approved message arrives, quickly streamed in word groups to preserve the typing effect.
4. 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](https://docs.sonecentral.com/en/sohbet-surekliligi.md#tekrar-denemek).

## 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](https://docs.sonecentral.com/en/insan-devri.md).

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:

```ts title="app/api/sohbet/route.ts"
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.
