# Chat Completions

Sayfa: https://docs.sonecentral.com/chat-completions
Markdown: https://docs.sonecentral.com/chat-completions.md

> POST /chat/completions ile OpenAI Chat Completions biçiminde mesaj gönderin; yanıtın sone alanı sohbetin kimliğini, durumunu ve zengin mesajları taşır.

```http
POST https://pb.sonecentral.com/api/v1/chat/completions
```

OpenAI Chat Completions'ın istek ve yanıt biçimini kullanır. OpenAI SDK'ları, LangChain ve OpenAI uyumlu her istemci `baseURL` değiştirilerek çalışır.

## İstek

```json
{
  "model": "default",
  "messages": [
    { "role": "user", "content": "Bu hafta sonu açık mısınız?" }
  ],
  "metadata": { "conversation_id": "conv_x8k2m9p4q7w1r3t" },
  "stream": false
}
```

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `model` | string | Asistanın kimliği ya da "default". Asistanlar ve modeller sayfası kimliklerin nasıl bulunduğunu anlatır. Zorunlu. |
| `messages` | array | Sohbetin mesajları. Son assistant mesajından sonraki user mesajları yeni mesaj sayılır, en fazla 12 tane. Öncekiler yalnız sohbeti bulmak ve yeni bir sohbeti başlatmak için okunur. Zorunlu. |
| `stream` | boolean | true ise yanıt sunucu tarafından gönderilen olaylarla, yani SSE ile akar. Varsayılan: false. |
| `stream_options` | object | include_usage: true ise akışın sonunda kullanım bilgisi gelir. |
| `conversation_id` | string | Sonecentral uzantısı. Sürdürülecek sohbetin kimliği. |
| `metadata` | object | Yalnız metadata.conversation_id okunur ve conversation_id ile aynı işi görür. Geri kalanı kullanılmaz, saklanmaz. |
| `user` | string | Yalnız sunucu anahtarıyla dikkate alınır. Kendi sisteminizdeki ziyaretçi kimliği; aynı değer aynı sohbeti bulur. |
| `safety_identifier` | string | user ile aynı işi görür. İkisi birden gelirse bu kullanılır. |

Sohbetin kimliği birden çok yoldan gelebilir. Sonecentral ilk dolu olanı şu sırayla kullanır:

1. Gövdedeki `conversation_id`.
2. Gövdedeki `sone.conversation_id`.
3. `X-Sone-Conversation-Id` başlığı.
4. `metadata.conversation_id`.
5. Geçmişe olduğu gibi eklediğiniz bir asistan mesajının `sone.conversation_id` alanı. Birden çok varsa en sonuncusu.

Kimlik hiç gelmezse sohbet başka yollarla aranır: [Sohbet sürekliliği](https://docs.sonecentral.com/sohbet-surekliligi.md#sohbet-nasil-bulunur).

### Mesaj rolleri

| Rol | Ne olur |
| --- | --- |
| `user` | Ziyaretçinin mesajı. `content` metin ya da `text` parçalarından oluşan bir dizi olabilir. |
| `assistant` | Önceki asistan yanıtı. Sohbeti bulmak için okunur. Sunucu anahtarıyla yeni bir sohbet açılırken geçmiş olarak da aktarılır. |
| `system`, `developer` | Sunucu anahtarıyla asistana yalnız bilgi olarak iletilir, en fazla 2.000 karakter. Tarayıcı anahtarıyla yok sayılır. Ayrıntılar: [Geliştirici bağlamı](https://docs.sonecentral.com/kimlik-dogrulama.md#gelistirici-baglami). |
| `tool`, `function` | Yok sayılır. |

Başka bir rol `400 invalid_role` alır. Son mesaj metin taşıyan bir `user` mesajı değilse istek `400 missing_user_message` alır. Yeni bir mesaj 4.000 karakterden uzunsa `400 message_too_long` gelir.

Yeni mesajda görsel, ses ya da dosya parçası varsa istek `400 unsupported_content` alır. Önceki mesajlardaki bu tür parçalar sessizce atlanır, yalnız metinleri okunur.

### Yok sayılan ve desteklenmeyen alanlar

Asistanın dil modelini, üslubunu ve araçlarını Sonecentral'daki ayarlar belirler. Bu yüzden şu alanlar **yok sayılır**, hata vermez: `temperature`, `top_p`, `max_tokens`, `max_completion_tokens`, `presence_penalty`, `frequency_penalty`, `seed`, `stop`, `logit_bias`, `logprobs`, `top_logprobs`, `reasoning_effort`, `verbosity`, `service_tier`, `store`, `parallel_tool_calls`, `tool_choice`, `function_call`, `audio`, `modalities`. Burada sayılmayan bilinmeyen alanlar da yok sayılır.

Şunlar **reddedilir**:

| Alan | Hata |
| --- | --- |
| `tools`, `functions` | `400 tools_not_supported`. Asistan işletmenin kendi araçlarını kullanır. |
| `n`, 1 dışında | `400 unsupported_parameter` |
| `response_format`, `text` dışında | `400 unsupported_parameter` |

## Yanıt

```json
{
  "id": "chatcmpl-9f2c4a7e1b8d3f6a0c5e2b9d",
  "object": "chat.completion",
  "created": 1760866000,
  "model": "k3m9x2p7q1w8e4r",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.",
        "refusal": null,
        "annotations": [],
        "sone": {
          "conversation_id": "conv_x8k2m9p4q7w1r3t",
          "status": "answered",
          "messages": [
            {
              "id": "msg_p4q7w1x8k2m9a5z",
              "role": "assistant",
              "type": "text",
              "text": "Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.",
              "created_at": 1760866000
            }
          ]
        }
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 7, "completion_tokens": 16, "total_tokens": 23 },
  "system_fingerprint": null,
  "sone": { "conversation_id": "conv_x8k2m9p4q7w1r3t", "status": "answered" }
}
```

`model` yanıt veren asistanın kimliğidir; `"default"` gönderseniz de seçilen asistanın kimliği gelir. Sohbetin kimliği ayrıca `X-Sone-Conversation-Id` yanıt başlığında gelir.

`content`, bu turda gönderilen bütün mesajların düz metnidir; birden çok mesaj araya boş satır, yani `\n\n` konularak birleştirilir. Yalnız `content` gösteren bir istemci de eksiksiz çalışır.

`usage` bilgi içindir; gönderilen yeni mesajların ve gelen metnin uzunluğundan, yaklaşık dört karakter bir belirteç sayılarak tahmin edilir. Faturalama bu sayılara göre yapılmaz.

### `sone` alanı

OpenAI biçimine eklenen alan budur. Tam hali `choices[0].message.sone` içinde gelir. Aynı özet, `messages` olmadan, yanıtın üst düzeyinde de vardır.

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `conversation_id` | string | Sohbetin kimliği, conv_ ile başlar. Sonraki isteklerde geri gönderin. |
| `status` | "answered" \| "handoff" \| "human" \| "silent" | Bu turun sonucu. Değerler aşağıdaki tabloda. |
| `messages` | SoneMessage[] | Bu turda gönderilen mesajlar, gönderildikleri sırayla. Biçimi Zengin mesajlar sayfasında. |

| `status` | Anlamı | `content` |
| --- | --- | --- |
| `answered` | Asistan yanıt verdi. | Yanıt metni |
| `handoff` | Sohbet bu turda ekibe geçti ve ziyaretçiye bir mesaj yazıldı, genellikle devri söyleyen mesaj. | Devir mesajı |
| `human` | Sohbet ekipte. Asistan yanıt vermedi; mesaj ekibe ulaştı. | Boş |
| `silent` | Yanıt gerekmedi, örneğin "teşekkürler" gibi bir kapanış. | Boş |

`handoff` ve `human` durumunda ekibin yanıtlarını nasıl okuyacağınız [İnsan devri ve ekip yanıtları](https://docs.sonecentral.com/insan-devri.md) sayfasında.

TypeScript'te bu alanları kendiniz tanımlayın:

```ts title="sone.ts"
import type { SoneMessage } from './sone-message'; // Zengin mesajlar sayfasındaki tanım

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

// Akışsız yanıtta choices[0].message.sone
export type SoneTurn = {
  conversation_id: string;
  status: SoneStatus;
  messages: SoneMessage[];
};

// Akışta bir parçanın üst düzeyindeki sone: her parçada yalnız biri dolu
export type SoneChunk = {
  message?: SoneMessage; // bir mesaj tamamlandı
  activity?: string; // asistan bir işe bakıyor: "Ürünlere bakılıyor"
  conversation_id?: string; // son parçada
  status?: SoneStatus; // son parçada
};

export const soneOfMessage = (message: unknown) => (message as { sone?: SoneTurn }).sone;
export const soneOfChunk = (chunk: unknown) => (chunk as { sone?: SoneChunk }).sone;
```

Python'da OpenAI modelleri bilinmeyen alanları korur:

```python
sone = completion.choices[0].message.to_dict().get("sone", {})
print(sone.get("conversation_id"), sone.get("status"))
```

## Akış

`"stream": true` ile yanıt `text/event-stream` olarak gelir. Parçalar şu sırayla gelir:

1. **İlk parça**, anahtar, sınır ve paket kontrolleri geçip asistan yanıtlamaya başladığında gelir: `delta` rolü taşır. Bu kontrollerden geçmeyen istek akış yerine sıradan bir HTTP hatası alır.
2. Asistan bir araç kullanırken **durum parçası** gelebilir: `delta` boştur, üst düzeydeki `sone.activity` "Ürünlere bakılıyor" gibi kısa bir metindir.
3. Her mesajın metni kelime grupları halinde `delta.content` olarak akar. İkinci ve sonraki mesajlar `\n\n` ile başlar. Bir mesajın **son parçası** ayrıca üst düzeyde `sone.message` taşır: o mesajın tamamı, `SoneMessage` biçiminde. Metni boş bir mesaj, `delta`'sı boş tek bir parçayla gelir.
4. **Son parça**: `finish_reason` `"stop"`, `delta` boş, üst düzeyde `sone` içinde `conversation_id` ve `status`.
5. `stream_options.include_usage` açıksa `choices` dizisi boş, `usage` dolu bir parça daha gelir. Bu durumda önceki bütün parçalarda `"usage": null` vardır.
6. Akış `data: [DONE]` ile biter.

`sone` her zaman parçanın üst düzeyindedir, `choices[0].delta` içinde değildir. Sohbetin kimliği akış başlarken `X-Sone-Conversation-Id` yanıt başlığında da gelir.

```txt
data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":null}],"sone":{"activity":"Çalışma saatlerine bakılıyor"}}

: ping

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"Evet, cumartesi 10.00 ile "},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"18.00 arası açığız. Pazar "},"logprobs":null,"finish_reason":null}]}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{"content":"kapalıyız."},"logprobs":null,"finish_reason":null}],"sone":{"message":{"id":"msg_p4q7w1x8k2m9a5z","role":"assistant","type":"text","text":"Evet, cumartesi 10.00 ile 18.00 arası açığız. Pazar kapalıyız.","created_at":1760866000}}}

data: {"id":"chatcmpl-9f2c","object":"chat.completion.chunk","created":1760866000,"model":"k3m9x2p7q1w8e4r","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"sone":{"conversation_id":"conv_x8k2m9p4q7w1r3t","status":"answered"}}

data: [DONE]
```

Örnekteki kimlikler kısaltıldı.

**OpenAI JS**

```ts
import type { SoneMessage } from './sone-message';
import { soneOfChunk } from './sone';

const stream = await client.chat.completions.create({
  model: 'default',
  messages: [{ role: 'user', content: 'Bu hafta sonu açık mısınız?' }],
  stream: true,
});

const messages: SoneMessage[] = [];
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? '');
  const sone = soneOfChunk(chunk);
  if (sone?.message) messages.push(sone.message); // zengin mesajı burada çizin
  if (sone?.activity) console.log(`\n${sone.activity}…`);
  if (sone?.status) console.log('\nsohbet:', sone.conversation_id, sone.status);
}
```

**OpenAI Python**

```python
stream = client.chat.completions.create(
    model="default",
    messages=[{"role": "user", "content": "Bu hafta sonu açık mısınız?"}],
    stream=True,
)

messages = []
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)
    sone = chunk.to_dict().get("sone") or {}
    if "message" in sone:
        messages.append(sone["message"])  # zengin mesajı burada çizin
    elif "activity" in sone:
        print(f"\n{sone['activity']}…")
    elif "status" in sone:
        print("\nsohbet:", sone["conversation_id"], sone["status"])
```

Akış başladıktan sonra bir hata olursa `data: {"error": {…}}` parçası ve ardından `data: [DONE]` gelir. OpenAI SDK'ları bu parçayı hata olarak fırlatır. Ayrıntılar: [Hatalar](https://docs.sonecentral.com/hatalar.md#akis-sirasinda-hata).

Metnin neden kelime kelime değil mesaj mesaj ilerlediği ve bağlantının nasıl canlı tutulduğu: [Akış](https://docs.sonecentral.com/akis.md).

## Tekrar denemek

Her yeni mesajda yeni bir `Idempotency-Key` gönderin, en fazla 200 karakter. Ağ hatasından sonra aynı mesajı yeniden gönderirken aynı değeri koyun. Sonecentral aynı mesajı ikinci kez kaydetmez: yanıt verilmişse aynı yanıt yeniden döner, asistan iki kez yanıt vermez. Aynı anahtarı başka bir metinle gönderirseniz istek `422 idempotency_key_reused` alır. Tarayıcı anahtarıyla, sohbetin kimliği olmadan başka bir ağ adresinden gelen deneme, örneğin sohbetin ilk mesajı, yeni sohbet açar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek).

OpenAI SDK'ları bu başlığı kendileri eklemez. İsteğin seçeneklerinde verin; SDK'nın kendi yeniden denemeleri de aynı başlığı gönderir:

**OpenAI JS**

```ts
const key = crypto.randomUUID(); // mesaj başına bir kez üretin, yeniden denerken aynısını kullanın

const completion = await client.chat.completions.create(
  {
    model: 'default',
    metadata: { conversation_id: 'conv_x8k2m9p4q7w1r3t' },
    messages: [{ role: 'user', content: 'Bu hafta sonu açık mısınız?' }],
  },
  { headers: { 'Idempotency-Key': key } },
);
```

**OpenAI Python**

```python
import uuid

key = str(uuid.uuid4())  # mesaj başına bir kez üretin, yeniden denerken aynısını kullanın

completion = client.chat.completions.create(
    model="default",
    metadata={"conversation_id": "conv_x8k2m9p4q7w1r3t"},
    messages=[{"role": "user", "content": "Bu hafta sonu açık mısınız?"}],
    extra_headers={"Idempotency-Key": key},
)
```

Bütün geçmişi ve sohbetin kimliğini her istekte gönderiyorsanız Sonecentral, ilk mesaj dışında, yeniden gönderilen mesajı geçmişin sonuna bakarak da tanır. Kimliği olmadan yalnız geçmişle gelen adsız bir deneme sohbeti bulmaz: yeni bir sohbet açar ve asistan yeniden yanıt verir. Sohbetin ilk mesajı, yalnız yeni mesajı gönderen ve kimliği saklamayan istemciler için tek güvenli yol anahtardır; bu yüzden her mesajda gönderin. Ayrıntılar: [Tekrar denemek](https://docs.sonecentral.com/sohbet-surekliligi.md#tekrar-denemek).

## Hatalar

| Durum | `code` | Ne zaman |
| --- | --- | --- |
| `400` | `missing_user_message` | Son mesaj metin taşıyan bir `user` mesajı değil. |
| `400` | `invalid_role` | Tanınmayan bir mesaj rolü. |
| `400` | `invalid_content` | Bir mesajın `content` alanı metin ya da parça dizisi değil. |
| `400` | `unsupported_content` | Yeni mesajda görsel, ses ya da dosya var. |
| `400` | `message_too_long` | Yeni bir mesaj 4.000 karakterden uzun. |
| `400` | `too_many_new_messages` | Son asistan mesajından sonra 12'den fazla `user` mesajı var. |
| `400` | `tools_not_supported` | `tools` ya da `functions` gönderildi. |
| `400` | `unsupported_parameter` | `n` 1 dışında ya da `response_format` `text` dışında. |
| `400` | `conversation_model_mismatch` | Sohbet başka bir asistanla açılmış. |
| `404` | `model_not_found` | Bu kimlikte, anahtarın çağırabildiği yayında bir asistan yok. |
| `404` | `conversation_not_found` | Gönderilen sohbet kimliği bu çalışma alanında bir API sohbeti değil. |
| `409` | `conversation_busy` | Aynı sohbette önceki mesaj hâlâ yanıtlanıyor. |
| `422` | `idempotency_key_reused` | Aynı `Idempotency-Key` başka bir metinle gönderildi. |

Bütün kodlar ve ne yapılacağı: [Hatalar](https://docs.sonecentral.com/hatalar.md).
