TekinAli ⎇ main

OpenAI Responses API'de token sayma, konuşma durumu ve 429 hataları

OpenAI API'nin nasıl değiştiği, bir istekten önce input token sayısının nasıl hesaplanacağı, konuşma durumunu korumanın üç yolu (manuel tekrar gönderim, previous_response_id, Conversations API) ve 429'da kota hatasını rate limit'ten ayırmak; Python ve C# örnekleriyle.

Büyük dil modeli üzerine kurulan her uygulama er ya da geç aynı üç problemi çözmek zorunda kalıyor. Bir isteğin kaç token tutacağını göndermeden önce bilmesi gerekiyor, ki context penceresinin ve bütçenin içinde kalabilsin. Bir konuşmayı turlar arasında taşıması gerekiyor. Ve 429 hatalarını, API'yi boğmadan ya da asla başarılı olmayacak bir isteği tekrar tekrar denemeden atlatması gerekiyor.

OpenAI API birkaç kez şekil değiştirdi ve üç konu için de eski kod örnekleri hâlâ yaygın olarak dolaşıyor. Bu yazıda her birinin güncel Responses API ile nasıl çalıştığını Python, ham HTTP ve C# örnekleriyle anlatıyorum.

API nasıl değişti

Eski örnek kodların neden artık çalışmadığını büyük ölçüde üç değişiklik açıklıyor.

  1. Python SDK v1 kırılması. openai 1.0.0, Kasım 2023'te çıktı ve openai.ChatCompletion.create(...) gibi modül seviyesindeki çağrıları bir client nesnesiyle değiştirdi: client = OpenAI() ve client.chat.completions.create(...). You tried to access openai.ChatCompletion hatasının tek sebebi bu değişiklik. openai==0.28'e pinlemek bir çözüm değil, saatli bir bomba.
  2. Chat Completions'tan Responses'a geçiş. OpenAI'ın migration guide'ı, Chat Completions'ın desteklenmeye devam ettiğini ama tüm yeni projeler için Responses'ın önerildiğini söylüyor. Responses mesajlar yerine item'larla çalışıyor, sistem seviyesindeki yönlendirme için instructions alıyor ve durumu sunucu tarafında saklayabiliyor.
  3. Assistants API'nin emekliye ayrılması. OpenAI'ın deprecations sayfası, Assistants API'nin 26 Ağustos 2026'da kaldırılacağını ve yerine Responses ile Conversations API'lerinin geçtiğini yazıyor. Thread ve run oluşturan bir kodun varsa ödünç zamanda yaşıyor, hatta o zaman çoktan dolmuş olabilir.

Göndermeden önce token saymak

Geleneksel yöntem, tiktoken ile yerelde tokenize edip rol işaretleri için mesaj başına elle ayarlanmış bir token sayısı eklemek. Bu, bilinen bir modelde düz metin için işe yarıyordu. Ama tool'lar, JSON şemaları, görseller ve dosyalar devreye girince dağılıyor, çünkü hepsi göremediğin token'lar ekliyor. Her yeni model ailesi de yeni bir sihirli sabit getirdi.

Bugün bunun için bir endpoint var. Token counting rehberine göre bu endpoint Responses API ile aynı input formatını kabul ediyor. Sayıma, yerel bir tokenizer'ın göremeyeceği rol ve mesaj sınırı gibi formatlama token'ları da dahil.

from openai import OpenAI

client = OpenAI()

payload = {
    "model": "your-model",
    "instructions": "Kısa cevap ver.",
    "input": [
        {"role": "user", "content": "SQLite'ta WAL modu nedir?"},
    ],
}

count = client.responses.input_tokens.count(**payload)
print(count.input_tokens)

if count.input_tokens < 50_000:
    response = client.responses.create(**payload)
    print(response.output_text)

Ham HTTP çağrısı script'lerde ve SDK'sı olmayan dillerde işe yarıyor:

curl https://api.openai.com/v1/responses/input_tokens \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model",
    "input": "SQLite WAL modu nedir?"
  }'

Şunu unutma: endpoint yalnızca input'u sayıyor. Modelin cevabında kaç token üreteceğini söyleyemiyor, o yüzden output için ayrıca bir bütçe belirle, örneğin max_output_tokens ile.

Bu endpoint yine de bir network çağrısı, o yüzden her tuş vuruşunda çağırma. Arayüzde "bu paragraf sığar mı" gibi kaba bir kontrol için tiktoken yeterli:

import tiktoken

enc = tiktoken.get_encoding("o200k_base")
approx = len(enc.encode(user_text))   # yalnızca düz metin: tool, görsel ve mesaj ek yükü yok

Sayı gerçek bir karara dayanak olacaksa endpoint'i kullan: geçmişi kırpmak, model seçmek ya da isteği reddetmek gibi. Yalnızca kabaca bir rakam lazımsa tiktoken kullan.

Konuşma durumunu korumak

Modelin kendisi stateless, bu yüzden geleneksel yöntem her seferinde bütün mesaj listesini göndermek. Bu hâlâ geçerli seçeneklerden biri. Ama artık üç seçenek var.

Seçenek 1: geçmişi kendin tekrar gönder

Geçmiş senin elinde. Kırpabilir ya da özetleyebilirsin, store=False ile de OpenAI tarafında sonradan erişilecek bir şey kalmaz. Conversation state rehberi, reasoning item'ları turlar arasında kaybolmasın diye yalnızca metni değil, tüm output dizisini input'a geri eklemeyi öneriyor.

history = [{"role": "user", "content": "WAL modunu iki cümleyle anlat."}]

r1 = client.responses.create(model="your-model", input=history, store=False)
history += r1.output                                   # tüm output item'larını koru

history.append({"role": "user", "content": "Peki synchronous=NORMAL neyi riske atar?"})
r2 = client.responses.create(model="your-model", input=history, store=False)
print(r2.output_text)

Seçenek 2: previous_response_id ile zincirleme

Response'lar varsayılan olarak saklanıyor (store varsayılanı true, API referansına göre de saklanan response verisi en az 30 gün tutuluyor). Bu sayede yalnızca yeni turu göndermen yetiyor:

r1 = client.responses.create(
    model="your-model",
    instructions="Kısa ve net konuşan bir veritabanı eğitmenisin.",
    input="WAL modunu iki cümleyle anlat.",
)

r2 = client.responses.create(
    model="your-model",
    previous_response_id=r1.id,
    instructions="Kısa ve net konuşan bir veritabanı eğitmenisin.",   # miras KALMAZ, tekrar gönder
    input="Peki synchronous=NORMAL neyi riske atar?",
)

Burada iki tuzak var. Birincisi, API referansına göre previous_response_id kullanırken önceki response'un instructions'ı bir sonrakine taşınmıyor. Tekrar göndermeyi unutursan system prompt'un ikinci turda sessizce kayboluyor. İkincisi, daha az gönderiyorsun ama daha az ödemiyorsun: rehber, zincirdeki önceki tüm input token'larının input token olarak faturalandığını belirtiyor. Zincirleme saklanan response'ların üzerine kurulu, o yüzden store=False ile saklamayı kapatıyorsan geçmişi kendin göndermeyi planla.

Seçenek 3: Conversations API

Oturumlar ya da cihazlar arasında yaşaması gereken bir sohbet için bir conversation nesnesi oluşturup response'ları ona bağlıyorsun. Konuşmadaki item'lar her isteğin başına ekleniyor, yeni input ve output da otomatik olarak konuşmaya yazılıyor. Rehbere göre conversation item'ları 30 günlük sınıra tabi değil. Aynı istekte conversation ile previous_response_id birlikte kullanılamıyor.

conv = client.conversations.create()

client.responses.create(model="your-model", conversation=conv.id,
                        input="WAL modunu iki cümleyle anlat.")
r = client.responses.create(model="your-model", conversation=conv.id,
                            input="Peki synchronous=NORMAL neyi riske atar?")
print(r.output_text)

Önerim şu: kişisel veri varsa ya da context üzerinde sıkı kontrol gerekiyorsa geçmişi kendin gönder, kısa tool döngülerinde previous_response_id kullan, ürün gerçekten uzun ömürlü bir sohbetse Conversations'a git.

Aynısı C# ile

Resmi OpenAI .NET kütüphanesi, Responses'ı ResponsesClient üzerinden sunuyor. Bu yazı yazılırken güncel olan 2.x sürümünde (2.14.0) bu tipler experimental olarak işaretli. Siz açıkça izin verene kadar derleyici OPENAI001 hatasıyla durduruyor. Ortak bir kütüphaneye koymadan önce bunu bilmekte fayda var.

#pragma warning disable OPENAI001   // Responses tipleri OpenAI 2.x'te [Experimental]
using OpenAI.Responses;

var client = new ResponsesClient(Environment.GetEnvironmentVariable("OPENAI_API_KEY")!);
const string Instructions = "Kısa ve net konuşan bir veritabanı eğitmenisin.";

var first = new CreateResponseOptions { Model = "your-model", Instructions = Instructions };
first.InputItems.Add(ResponseItem.CreateUserMessageItem("WAL modunu iki cümleyle anlat."));
ResponseResult r1 = await client.CreateResponseAsync(first);
Console.WriteLine(r1.GetOutputText());

var second = new CreateResponseOptions
{
    Model = "your-model",
    PreviousResponseId = r1.Id,
    Instructions = Instructions,          // tekrar gönder: taşınmıyor
};
second.InputItems.Add(ResponseItem.CreateUserMessageItem("Peki synchronous=NORMAL neyi riske atar?"));
ResponseResult r2 = await client.CreateResponseAsync(second);
Console.WriteLine(r2.GetOutputText());

store: false'un .NET'teki karşılığı StoredOutputEnabled = false.

429 hataları: kota ve rate limit

429, aynı status code'u paylaşan çok farklı iki problem anlamına gelebiliyor. Güncel error codes rehberi 429'u birkaç duruma ayırıyor. Kredi bakiyesinin bitmesi, organizasyon ya da proje harcama limitine ulaşılması ve kullanım limitinin dolması fatura problemleri. Rehber bu konuda net: fatura, harcama ya da kota hatalarında retry yapmak erişimi geri getirmiyor. "Rate limit reached" ve slow_down ise hız problemleri. Bunlarda varsa Retry-After header'ına uy ve beklemeyi artırarak tekrar dene.

İki SDK da 429'ları zaten exponential backoff ile tekrar deniyor: Python kütüphanesi varsayılan olarak iki kez (max_retries), .NET kütüphanesi ise üç kereye kadar. Yani senin işin çoğunlukla bu iki durumu ayırt etmek ve fatura hatalarında hızlıca hata vermek:

import openai
from openai import OpenAI

client = OpenAI(max_retries=4)   # backoff ve Retry-After'ı SDK yönetiyor

BILLING = {"insufficient_quota", "credit_balance_exhausted",
           "organization_spend_limit_exceeded", "project_spend_limit_exceeded",
           "organization_usage_limit_exceeded"}

try:
    r = client.responses.create(model="your-model", input="ping")
except openai.RateLimitError as e:
    if e.code in BILLING or e.type == "insufficient_quota":
        raise RuntimeError("Fatura ya da kota problemi, retry işe yaramaz") from e
    raise   # SDK'nın tüm denemelerinden sonra gerçek rate limit: yükü azalt ya da kuyruğa al

Kontrol listesi

  • Kod tabanında openai.ChatCompletion ya da openai==0.28 pin'i kalmadı.
  • Yeni kod Responses üzerinde. Assistants API'de hiçbir şey kalmadı.
  • Karar veren token sayıları /v1/responses/input_tokens'tan geliyor. tiktoken yalnızca tahmin için.
  • previous_response_id kullanırken instructions her turda yeniden gönderiliyor.
  • store konusunda bilinçli bir seçim var: varsayılan response'ları saklıyor, verinin kalmaması gereken yerlerde store=False.
  • 429 yönetimi fatura hatalarını rate limit'ten ayırıyor.
  • Model isimleri kodda sabit değil, config'te. Yukarıdakilerin hepsinden daha hızlı değişiyorlar.

Bu API ile yaşanan sürtünmenin çoğu, onun eski bir sürümü için yazılmış koddan geliyor. Bir örneği kopyalamadan önce hangi API ve SDK sürümünü hedeflediğine bak.

tekinali.db · salt okunur · ● bağlı

SQLite 3.53.3 · 8 tablo, 2 view.

Soldaki bir tabloya tıkla, aşağıdan bir sorgu seç ya da kendin yaz. Yardım: \?

↑↓ seç · Enter aç · Esc kapat