# Akış

Sayfa: https://docs.sonecentral.com/akis
Markdown: https://docs.sonecentral.com/akis.md

> Akışlı yanıtlar nasıl gelir, bağlantı nasıl canlı tutulur ve metin neden kelime kelime değil mesaj mesaj ilerler.

Üç biçim de akışı destekler: Chat Completions ve Responses'ta `"stream": true`, Vercel AI SDK'da `/chat` her zaman akar. Hepsi **sunucu tarafından gönderilen olaylar**, yani SSE kullanır.

## Bağlantı

Akışlı yanıtın başlıkları:

```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 akışında ayrıca `x-vercel-ai-ui-message-stream: v1` gelir.

- **Önce kontroller.** Anahtar, istek sınırı, asistan, sohbet ve paket kontrolleri akış başlamadan yapılır. Geçmeyen istek akış yerine sıradan bir HTTP hatası alır: [Hatalar](https://docs.sonecentral.com/hatalar.md).
- **İlk olay asistan başlarken gelir.** Kontroller geçince Chat Completions'ta rolü taşıyan ilk parça, Responses'ta `response.created`, AI SDK'da `start` hemen gelir. Asistan daha düşünürken bağlantının kurulduğunu ve sohbetin kimliğini bilirsiniz.
- **On saniyede bir kalp atışı.** Uzun süren turlarda bağlantı `: ping` satırlarıyla canlı tutulur. İki nokta üst üste ile başlayan satırlar SSE yorumudur; SDK'lar onları zaten atlar, kendi ayrıştırıcınızı yazıyorsanız siz de atlayın.
- **Bitiş.** Chat Completions ve AI SDK `data: [DONE]` ile biter. Responses `response.completed` ya da, bir hatada, `response.failed` olayıyla biter; orada `[DONE]` yoktur.

## Metin neden mesaj mesaj gelir

Genel bir dil modeli API'sinde metin model ürettikçe, kelime kelime gelir. Burada öyle değil; bu bilerek böyle.

Asistanın yazdığı yanıt ziyaretçiye gitmeden önce işletmenin güvenlik kontrollerinden geçer: fiyat ve stok doğru mu, söz verilmemesi gereken bir şey var mı, mesaj gerçekten gönderilmeli mi. Bu kontroller yanıtın **tamamına** bakar. Bu yüzden bir mesaj ancak bitip kontrolden geçtikten sonra yola çıkar.

Sonuçta akış şöyle ilerler:

1. Kontroller geçer, bağlantı açılır, ilk olay gelir.
2. Asistan düşünür ve araçlarını kullanır. Bu birkaç saniye sürebilir. Bu sırada "Ürünlere bakılıyor" gibi kısa durumlar gelir.
3. İlk mesaj kontrolden geçer ve gelir. Daktilo etkisi bozulmasın diye kelime gruplarına bölünerek hızla akar.
4. Varsa sonraki mesajlar, ürün kartları ve düğmeler aynı şekilde sırayla gelir.

Kısa durumlar her biçimde başka bir yerde gelir:

| Biçim | Durum |
| --- | --- |
| Chat Completions | `delta`'sı boş bir parçanın üst düzeyindeki `sone.activity` |
| Responses | Bir olay değil, SSE yorum satırı: `: sone.activity {"label":"Ürünlere bakılıyor"}`. `sequence_number` taşımaz; SDK'lar onu atlar, okumak için akışı kendiniz ayrıştırın. |
| Vercel AI SDK | Geçici (`transient: true`) `data-sone-status` parçasının `data.label` alanı; yalnız `onData`'ya gelir |

Durum metni bir araç adı değil, ziyaretçinin anlayacağı Türkçe bir cümledir. İki mesaj arasında kısa bir bekleme olabilir. Arayüzünüzde bir "yazıyor" göstergesi tutmak iyi olur.

Aynı mesajı aynı `Idempotency-Key` ile, ya da sohbetin kimliği ve aynı geçmişle yeniden gönderdiğinizde asistan yeniden yanıt vermez; kayıtlı yanıt akışın sonunda bir kerede gelir. Ayrıntılar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek).

## Bağlantı koparsa

Asistanın gönderdiği her mesaj, gönderildiği anda sohbete kaydedilir. Akış yarıda koparsa kaçırdığınız mesajları `GET /conversations/{id}/messages` ile okuyabilirsiniz. Sohbetin kimliği akışın başında `X-Sone-Conversation-Id` başlığında gelir. Ayrıntılar: [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md).

Aynı mesajı yeniden göndermeniz gerekirse ilk gönderimdeki `Idempotency-Key` ile gönderin; asistan iki kez yanıt vermez. Önceki tur hâlâ yanıtlanıyorsa istek `409 conversation_busy` alır.

## Proxy arkasında

Akışı kendi sunucunuzdan geçiriyorsanız ara katmanın yanıtı tamponlamadığından emin olun. Nginx'te `proxy_buffering off;` yeterlidir. Next.js route handler'larında yanıt gövdesini olduğu gibi döndürün:

```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') ?? '',
    },
  });
}
```

Bu şekilde sunucu anahtarını kullanırsanız, isteği gönderen ziyaretçiyi kendi tarafınızda doğrulamak ve sınırlamak sizin işinizdir. Sitenizdeki sohbet penceresi için tarayıcı anahtarı daha basittir.
