# Vercel AI SDK

Sayfa: https://docs.sonecentral.com/vercel-ai-sdk
Markdown: https://docs.sonecentral.com/vercel-ai-sdk.md

> useChat ile tarayıcıdan doğrudan /chat uç noktasına bağlanın, zengin mesajları tipli veri parçaları olarak çizin; ya da sunucuda OpenAI sağlayıcısıyla generateText ve streamText kullanın.

Vercel AI SDK ile iki yol var:

- **`useChat` ile doğrudan.** Sitenizdeki sohbet penceresi tarayıcı anahtarıyla `POST /chat` uç noktasına bağlanır. Arada sizin sunucunuz yoktur. Ürün kartları, düğmeler ve dosyalar tipli veri parçaları olarak gelir.
- **Sağlayıcı olarak.** Sunucu kodunuzda `@ai-sdk/openai` ya da `@ai-sdk/openai-compatible` ile `generateText` ve `streamText` kullanırsınız.

Örnekler `ai` 7, `@ai-sdk/react` 4, `@ai-sdk/openai` 4 ve `@ai-sdk/openai-compatible` 3 ile denendi. `/chat` akışı AI SDK 5 ve sonrasının UI mesaj akışı biçimindedir.

## `useChat` ile doğrudan

```npm
npm i ai @ai-sdk/react
```

```tsx title="components/chat.tsx"
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

const transport = new DefaultChatTransport({
  api: 'https://pb.sonecentral.com/api/v1/chat',
  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` },
  body: { model: 'default' },
});

export function Chat() {
  const [input, setInput] = useState('');
  const { messages, sendMessage, status, error } = useChat({ transport });
  const busy = status === 'submitted' || status === 'streaming';

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) =>
            part.type === 'text' ? <p key={index}>{part.text}</p> : null,
          )}
        </div>
      ))}
      {error ? <p>{error.message}</p> : null}
      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (!input.trim() || busy) return;
          sendMessage({ text: input });
          setInput('');
        }}
      >
        <input value={input} onChange={(event) => setInput(event.target.value)} disabled={busy} />
      </form>
    </div>
  );
}
```

`NEXT_PUBLIC_SONE_PUBLISHABLE_KEY` bir **tarayıcı anahtarı** olmalı, yani `pk-sone-v1-…` ile başlamalı. Sitenizin adresi de anahtarın izin verilen siteleri arasında bulunmalı. Sunucu anahtarı burada `403 secret_key_in_browser` alır.

Bu en kısa örnek yalnız metin parçalarını çizer. Ürün kartları, düğmeler ve dosyalar `data-sone-*` parçaları olarak gelir; onları çizen örnek aşağıda: [Tipli mesajlar](#tipli-mesajlar).

Girdi yalnız bir mesaj yanıtlanırken kapalıdır. Bir hatadan sonra `status` `error` olur, `error.message` Türkçe açıklamayı taşır ve ziyaretçi yeniden yazabilir.

### İstek gövdesi

`DefaultChatTransport` her mesajda şunu gönderir; `body` içine koyduklarınız da eklenir:

```json
{
  "id": "chat_l7k2…",
  "messages": [{ "id": "…", "role": "user", "parts": [{ "type": "text", "text": "Merhaba" }] }],
  "trigger": "submit-message",
  "model": "default"
}
```

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `id` | string | useChat'in sohbet kimliği. Tarayıcının seçtiği bir değerdir; sohbeti yalnız gönderdiğiniz mesajlar kayıttaki son ziyaretçi mesajı ve yanıtıyla bitiyorsa sürdürür. |
| `messages` | UIMessage[] | Sohbetin mesajları. text parçaları, asistan mesajlarında data-sone-* parçalarının data.text alanı da okunur. Son asistan mesajından sonraki user mesajları yeni mesajdır; yeni mesajda dosya parçası varsa istek reddedilir. |
| `model` | string | Asistanın kimliği. X-Sone-Model başlığıyla da verilebilir. Varsayılan: "default". |
| `conversationId` | string | Sürdürülecek sohbetin kimliği. X-Sone-Conversation-Id başlığıyla da verilebilir. |
| `user` | string | Yalnız sunucu anahtarıyla dikkate alınır. Kendi sisteminizdeki ziyaretçi kimliği. |
| `context` | string | Yalnız sunucu anahtarıyla dikkate alınır. Asistana yalnız bilgi olarak iletilen geliştirici bağlamı, en fazla 2.000 karakter. |

`trigger` ve `messageId` alanları okunmaz.

### Sohbetin sürmesi

Her asistan mesajının `metadata.conversationId` alanında sohbetin kimliği vardır. Mesajlar her istekte geri gittiği için sohbet onunla bulunur. Sohbetin kimliği şu sırayla okunur: gövdedeki `conversationId`, `X-Sone-Conversation-Id` başlığı, son asistan mesajının `metadata.conversationId` alanı.

Sayfa yenilendiğinde sohbeti sürdürmek için `useChat({ id, messages })` ile hem kimliği hem mesajları, asistan mesajlarının `metadata` alanıyla birlikte geri verin. Yalnız `id` yetmez: tarayıcının seçtiği kimlik sohbeti yalnız sohbet asistandayken, son yanıtı asistan kendisi yazmışken ve geri verdiğiniz mesajlar sohbetin ilk mesajından başlıyorsa sürdürür; sohbet ekipteyken, son yanıt hazır bir cümle, karşılama mesajı veya ekibinizin bir mesajıyken ya da mesajlar kısaltılmışken yeni bir sohbet açılır. Saklama örneği: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md#vercel-ai-sdkda).

Bir mesaj yeniden gönderildiğinde mesajın kendi `id`'si tekrar denemeyi tanır: aynı mesaj ikinci kez kaydedilmez, asistan iki kez yanıt vermez. Kimliği aynı kalıp metni değişen, yani düzenlenen bir mesaj yeni bir mesaj sayılır. Tarayıcı anahtarıyla, sohbetin kimliği olmadan başka bir ağ adresinden gelen deneme yeni sohbet açar. Bu ilk mesajda olur, bir de asistan mesajlarının `metadata.conversationId` alanını sakladığınız geçmişte tutmuyorsanız: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek).

### Uzun sohbetlerde yalnız son mesajı göndermek

İstek gövdesi en fazla 1 MB olabilir ve gerçek kayıt Sonecentral'dadır. Sohbetin kimliği elinizdeyse yalnız son mesajı gönderebilirsiniz:

```ts title="lib/transport.ts"
import { DefaultChatTransport } from 'ai';
import type { SoneUIMessage } from '@/lib/sone';

export const transport = new DefaultChatTransport<SoneUIMessage>({
  api: 'https://pb.sonecentral.com/api/v1/chat',
  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` },
  prepareSendMessagesRequest: ({ id, messages }) => {
    const conversationId = messages.findLast((m) => m.metadata?.conversationId)?.metadata?.conversationId;
    return {
      body: {
        id,
        model: 'default',
        conversationId,
        // Kimlik varsa yalnız ziyaretçinin yeni mesajı yeter; yoksa bütün geçmiş gider.
        messages: conversationId ? messages.slice(-1) : messages,
      },
    };
  },
});
```

`prepareSendMessagesRequest` verildiğinde `body` seçeneği kullanılmaz; `model`'i dönen gövdeye kendiniz yazın. Mesajın kendi `id`'si gittiği için yeniden deneme bu yolda da güvenlidir.

### Akışta neler gelir

Yanıt, AI SDK'nın UI mesaj akışı biçimindedir ve `x-vercel-ai-ui-message-stream: v1` başlığını taşır:

| Parça | Ne zaman |
| --- | --- |
| `start` | Kontroller geçip asistan yanıtlamaya başladığında. `messageMetadata` içinde `conversationId` ve `model` gelir. |
| `start-step` | `start`'ın hemen ardından. |
| `text-start`, `text-delta`, `text-end` | Asistanın her düz metin mesajı için bir kez. `id` mesajın Sonecentral'daki kimliğidir. |
| `data-sone-catalog` | Ürün ya da hizmet kartları. |
| `data-sone-actions` | Düğmeler ya da hızlı yanıtlar. |
| `data-sone-file` | Paylaşılan bir dosya. |
| `data-sone-staff` | Ekibinizden birinin bu tur sırasında yazdığı düz metin mesajı. |
| `data-sone-status` | Asistanın o an ne yaptığı, örneğin "Ürünlere bakılıyor". `transient: true` taşır: mesaja eklenmez, yalnız `onData`'ya gelir. |
| `finish-step`, `finish` | Tur bitti. `finish` parçasının `messageMetadata`'sında `conversationId`, `model` ve `status` vardır. |
| `error` | Akış başladıktan sonra bir hata oldu. `errorText` Türkçe açıklamadır. |

Akış `data: [DONE]` ile biter. Bekleme sırasında `: ping` yorum satırları gelir; AI SDK onları atlar.

Zengin parçaların `data` alanı, çizmek için gerekeni taşır:

```json
{
  "type": "data-sone-catalog",
  "id": "msg_c4t2l9g7x1p3h8q",
  "data": {
    "text": "Keten gömlek, taş rengi\n1.290,00 TL\nM ve L stokta\nhttps://www.ornek.com/urun/keten-gomlek-tas",
    "role": "assistant",
    "catalog": {
      "format": "card",
      "items": [
        {
          "id": "r8k2m9p4q7w1x3z",
          "kind": "inventory",
          "title": "Keten gömlek, taş rengi",
          "price": "1.290,00 TL",
          "detail": "M ve L stokta",
          "url": "https://www.ornek.com/urun/keten-gomlek-tas",
          "button_title": "Ürünü incele"
        }
      ]
    }
  }
}
```

Parçanın `id`'si mesajın Sonecentral'daki kimliğidir; sohbetin mesaj listesindeki `id` ile aynıdır. `data.text` düz metin karşılığıdır. `data.role` `assistant` ya da `staff` olur: ekibiniz de kart, düğme ya da dosya gönderebilir. Türüne göre `catalog`, `actions` ya da `file` alanlarından biri doludur; biçimleri [Zengin mesajlar](https://docs.sonecentral.com/zengin-mesajlar.md) sayfasındadır.

### Tipli mesajlar

```ts title="lib/sone.ts"
import type { UIMessage } from 'ai';

// SoneMessage, SoneCatalog, SoneActions ve SoneFile: Zengin mesajlar sayfasındaki tanımları bu dosyaya ekleyin.

export type SoneStatus = 'answered' | 'handoff' | 'human' | 'silent';

// /chat akışındaki zengin parçaların data alanı
export type SoneDataPart = {
  text: string; // düz metin karşılığı
  role: 'assistant' | 'staff';
  catalog?: SoneCatalog;
  actions?: SoneActions;
  file?: SoneFile;
};

export type SoneUIMessage = UIMessage<
  {
    conversationId?: string;
    model?: string;
    status?: SoneStatus;
  },
  {
    'sone-catalog': SoneDataPart;
    'sone-actions': SoneDataPart;
    'sone-file': SoneDataPart;
    'sone-staff': { text: string; role: 'staff' };
    'sone-status': { label: string };
  }
>;
```

```tsx title="components/chat.tsx"
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
import type { SoneUIMessage } from '@/lib/sone';

const transport = new DefaultChatTransport<SoneUIMessage>({
  api: 'https://pb.sonecentral.com/api/v1/chat',
  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SONE_PUBLISHABLE_KEY}` },
  body: { model: 'default' },
});

export function Chat() {
  const [activity, setActivity] = useState('');
  const { messages, sendMessage } = useChat<SoneUIMessage>({
    transport,
    onData: (part) => {
      if (part.type === 'data-sone-status') setActivity(part.data.label);
    },
    onError: () => setActivity(''),
    onFinish: () => setActivity(''),
  });

  return (
    <div>
      {messages.map((message) =>
        message.parts.map((part, index) => {
          const key = `${message.id}-${index}`;
          switch (part.type) {
            case 'text':
              return <p key={key}>{part.text}</p>;
            case 'data-sone-staff':
              return (
                <p key={key}>
                  <strong>Ekip: </strong>
                  {part.data.text}
                </p>
              );
            case 'data-sone-catalog':
              return (
                <ul key={key}>
                  {part.data.catalog?.items.map((item) => (
                    <li key={item.id}>
                      <strong>{item.title}</strong> {item.price}
                      {item.url ? (
                        <a href={item.url} target="_blank" rel="noreferrer">
                          {item.button_title || 'İncele'}
                        </a>
                      ) : null}
                    </li>
                  ))}
                </ul>
              );
            case 'data-sone-actions': {
              const actions = part.data.actions;
              return (
                <div key={key}>
                  {actions?.text ? <p>{actions.text}</p> : null}
                  {actions?.buttons?.map((button) => (
                    <a key={button.title} href={button.url} target="_blank" rel="noreferrer">
                      {button.title}
                    </a>
                  ))}
                  {actions?.choices?.map((choice) => (
                    <button key={choice} onClick={() => sendMessage({ text: choice })}>
                      {choice}
                    </button>
                  ))}
                </div>
              );
            }
            case 'data-sone-file':
              return part.data.file?.url ? (
                <a key={key} href={part.data.file.url} download>
                  {part.data.file.name}
                </a>
              ) : (
                <p key={key}>{part.data.text}</p>
              );
            default:
              return null;
          }
        }),
      )}
      {activity ? <p aria-live="polite">{activity}…</p> : null}
    </div>
  );
}
```

URL'si olmayan bir düğmeye ya da hızlı yanıta tıklanınca, başlığını ziyaretçinin yeni mesajı olarak gönderin. Asistan onu ziyaretçinin yazdığı bir mesaj gibi okur.

### Sohbetin kimliği ve durumu

```ts
const last = messages.at(-1);
const conversationId = last?.metadata?.conversationId;
const status = last?.metadata?.status; // 'human' ise ekip yazıyor
```

`status` `handoff` ya da `human` olduğunda ekibin sonraki yanıtları akışa gelmez; onları [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md) sayfasındaki gibi okuyun. Sohbetin şu an kimde olduğunu `GET /conversations/{id}` söyler. Tam bir pencere örneği: [Site içine sohbet penceresi](https://docs.sonecentral.com/sohbet-penceresi.md#react-ve-nextjs).

### Hatalar

`/chat` akış başlamadan bir isteği reddederse gövde JSON değil, düz Türkçe metindir. AI SDK bu metni olduğu gibi `useChat`'in `error.message` değerine koyar. Hatanın kodu `X-Sone-Error-Code` başlığında gelir; bu başlık tarayıcıdaki koda açılmaz, yalnız sunucudan yapılan isteklerde okunur. Akış başladıktan sonraki bir hata `error` parçasıyla gelir; `error.message` yine Türkçe açıklamadır. Kodların listesi: [Hatalar](https://docs.sonecentral.com/hatalar.md).

### Bağlantı koparsa

`useChat({ resume: true })` açılışta `GET /chat/{id}/stream` ister. Sonecentral bu isteğe anahtarı denetledikten sonra her zaman `204` döner: sürdürülecek bir akış yoktur, çünkü bir turun mesajları gönderildikleri anda sohbete kaydedilir. Kopan bir bağlantıdan sonra kaçırdığınız mesajları `GET /conversations/{id}/messages` ile okuyun.

## Sunucuda sağlayıcı olarak

Sunucu kodunuzda asistanı herhangi bir AI SDK modeli gibi kullanın. Sunucu anahtarı burada güvendedir.

```npm
npm i ai @ai-sdk/openai
```

**@ai-sdk/openai**

```ts
import { createOpenAI } from '@ai-sdk/openai';
import { generateText, streamText } from 'ai';

const sone = createOpenAI({
  baseURL: 'https://pb.sonecentral.com/api/v1',
  apiKey: process.env.SONE_API_KEY,
});

// Chat Completions uç noktası
const { text } = await generateText({
  model: sone.chat('default'),
  prompt: 'Kargo kaç günde gelir?',
});

// Responses uç noktası
const result = streamText({
  model: sone.responses('default'),
  prompt: 'İade nasıl yapılıyor?',
});

for await (const delta of result.textStream) {
  process.stdout.write(delta);
}
```

**@ai-sdk/openai-compatible**

```ts
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { generateText } from 'ai';

const sone = createOpenAICompatible({
  name: 'sone',
  baseURL: 'https://pb.sonecentral.com/api/v1',
  apiKey: process.env.SONE_API_KEY,
});

const { text } = await generateText({
  model: sone.chatModel('default'),
  prompt: 'Kargo kaç günde gelir?',
});
```

Sohbetin kimliği her yanıtın `X-Sone-Conversation-Id` başlığında gelir. Sonraki istekte aynı başlıkla geri gönderin:

```ts
const first = await generateText({
  model: sone.chat('default'),
  prompt: 'Kargo kaç günde gelir?',
});
const conversationId = first.response.headers?.['x-sone-conversation-id'];

const second = await generateText({
  model: sone.chat('default'),
  headers: conversationId ? { 'X-Sone-Conversation-Id': conversationId } : undefined,
  prompt: 'Peki iade süresi?',
});
```

`@ai-sdk/openai`'nin Responses modeliyle önceki turun mesajlarını geri verirseniz sağlayıcı önceki çıktıları `item_reference` olarak gönderir ve sohbet onlarla da bulunur:

```ts
const turn1 = await generateText({ model: sone.responses('default'), prompt: 'İade koşullarınız nedir?' });

const turn2 = await generateText({
  model: sone.responses('default'),
  messages: [
    { role: 'user', content: 'İade koşullarınız nedir?' },
    ...turn1.response.messages,
    { role: 'user', content: 'Kaç gün içinde?' },
  ],
});
```

Sağlayıcı yolu yalnız metni verir. Ürün kartlarını ve düğmeleri okumak istiyorsanız OpenAI SDK'sını ya da yukarıdaki `useChat` yolunu kullanın. AI SDK'nın araç çağırma özelliği, yani `tools`, desteklenmez: asistanın araçlarını Sonecentral yönetir.
