Vercel AI SDK
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:
useChatile doğrudan. Sitenizdeki sohbet penceresi tarayıcı anahtarıylaPOST /chatuç 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/openaiya da@ai-sdk/openai-compatibleilegenerateTextvestreamTextkullanı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 i ai @ai-sdk/react'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.
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:
{
"id": "chat_l7k2…",
"messages": [{ "id": "…", "role": "user", "parts": [{ "type": "text", "text": "Merhaba" }] }],
"trigger": "submit-message",
"model": "default"
}Alan
Tür
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.
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.
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:
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:
{
"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 sayfasındadır.
Tipli mesajlar
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 };
}
>;'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
const last = messages.at(-1);
const conversationId = last?.metadata?.conversationId;
const status = last?.metadata?.status; // 'human' ise ekip yazıyorstatus handoff ya da human olduğunda ekibin sonraki yanıtları akışa gelmez; onları İnsan devri ve ekip yanıtları 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.
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.
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 i ai @ai-sdk/openaiimport { 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);
}Sohbetin kimliği her yanıtın X-Sone-Conversation-Id başlığında gelir. Sonraki istekte aynı başlıkla geri gönderin:
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:
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.