Yapay zekâ entegrasyonu
Cloudflare Workers’ta LLM çağrıları nasıl yönetilir? OpenAI–Claude yedeği
FeedHand’de model anahtarları ayrı bir gateway Worker’ında duruyor, OpenAI önce deneniyor, Claude devralıyor ve uzun işler 6 Cloudflare Workflow’unda dayanıklı adımlarla koşuyor.


KISA CEVAP
FeedHand’de uygulama Worker’ı sağlayıcı anahtarı görmüyor; her çağrı, genel URL’si olmayan feedhand-ai-gateway Worker’ına servis bağlamasıyla gidiyor. Gateway en kötü maliyeti bir Durable Object’te çağrıdan önce ayırıyor, sonra gerçek maliyete kapatıyor. Çağrı önce OpenAI’a gidiyor; 429, 5xx ya da bağlantı hatasında konuşma bir kez Claude’a geçiyor. Kim harcayabilir sorusunun cevabı tek bir saf fonksiyonda, server/spend-access.ts içinde.
Model çağrıları neden uygulama Worker’ında değil, ayrı bir Worker’da?
FeedHand, bir web adresinden bir aylık sosyal medya planı ve gönderileri çıkaran kendi SaaS ürünümüz. TanStack Start ile yazılmış web uygulaması feedhand-web adlı Worker’da çalışıyor; model çağrıları ise ikinci bir Worker’da, feedhand-ai-gateway’de. Monorepo’da iki uygulama ve 14 paket var; gateway bunlardan sadece @feedhand/ai paketine bağımlı.
Gateway’in wrangler.jsonc dosyasında workers_dev: false, preview_urls: false ve hiç route yok. Yani internetten ulaşılabilecek bir adresi yok. Tek giriş, aynı hesaptaki web Worker’ından gelen AI_GATEWAY servis bağlaması. Buna ek olarak her istek GATEWAY_TOKEN taşımak zorunda ve karşılaştırma sabit zamanlı yapılıyor.
İşin pratik kısmı şu: uygulama Anthropic SDK’sını ve OpenAI istemcisini olduğu gibi kullanıyor. Token, SDK’nın API anahtarını koyduğu yere gidiyor (x-api-key ya da Authorization: Bearer), gateway onu gerçek anahtarla değiştiriyor. SDK’ya yalnızca farklı bir baseURL ve isteği servis bağlamasından geçiren bir fetch veriliyor.
Bağlamada "remote": true var. Yerelde vite dev çalışırken bile istekler canlı gateway’e gidiyor, dolayısıyla geliştirici makinesinde hiçbir sağlayıcı anahtarı bulunmuyor. Dört anahtar (Anthropic, OpenAI, gateway token’ı ve medya API’si) yalnızca gateway’in gizli ayarlarında duruyor.
Gateway bir isteği sağlayıcıya göndermeden önce neyi kontrol ediyor?
Gateway bir proxy değil, bir politika katmanı. Kurallar apps/ai-gateway/src/policy.ts içinde ve hepsi saf fonksiyon, bu yüzden Worker çalışma zamanı olmadan test ediliyor. Bazı somut sınırlar:
- Claude tarafında yalnızca izin listesindeki üç model kabul ediliyor;
max_tokensen çok 64.000, OpenAI tarafında 32.000. - Gövde en çok 4 MiB, mesaj sayısı 1 ile 12 arasında.
- Çağıranın anahtarımız üzerinden araçlara ya da kod çalıştırmaya ulaşmasını sağlayacak alanlar reddediliyor. İzin verilen tek araç web araması; çağrı başına en çok 8 arama ve arama başı 0,01 $ olarak hesaplanıyor.
- OpenAI’a giden her gövdeye
store: falseekleniyor. - Ses dosyası yazıya çevirmede form yeniden kuruluyor; yalnızca kontrol edilmiş beş alan OpenAI’a gidiyor.
Uç noktalar da sınırlı: /anthropic/v1/messages, /openai/v1/chat/completions, yalnızca web araması için /openai/v1/responses, /openai/v1/audio/transcriptions ve harcamayı gösteren GET /usage. Bunların dışındaki her yol 404 dönüyor.
Loglama kuralı kodda yorum olarak yazılı: yalnızca metadata, asla istem, çıktı ya da anahtar. Bir log satırında sağlayıcı, şerit, model, HTTP durumu, dolar maliyeti ve marka kimliği var. Bu, hangi markanın ne harcadığını izlemeye yetiyor ve müşteri içeriğini loglara taşımıyor.
Paralel çağrılar bütçeyi nasıl aşamıyor?
Harcama defteri tek bir Durable Object örneği: Budget, idFromName("global") ile açılıyor ve SQLite depolamasında spend ve credits adlı iki tablo tutuyor. Tek örnek olduğu için her metot bir sonrakinden önce tamamlanıyor; farklı izolatlardan gelen istekler de aynı sıraya giriyor.
Akış iki adımlı. Önce rezervasyon: sağlayıcı çağrılmadan en kötü maliyet ayrılıyor. Anthropic için bu tahmin her üç bayt girdiyi bir token sayıyor ve çıktı bütçesinin tamamını ekliyor; gerçek metin token başına 3–4 bayt olduğu için tahmin hep yukarıda kalıyor. Sonra kapatma: sağlayıcı cevap verince satır gerçek maliyetle güncelleniyor. Hatalı istek 0’a kapanıyor çünkü sağlayıcı faturalamıyor. Kopan bir akış ya da çözümlenemeyen bir cevap ise en kötü tahminle kalıyor; sınırlar güvenli tarafta hata yapıyor.
Rezervasyon birkaç sınıra aynı anda bakıyor. Küresel günlük 40 $ ve aylık 600 $, herkese açık ücretsiz araçlar için günlük 3 $, ücretsiz önizleme şeridi için günlük 25 $ ve tek bir önizlemenin ömür boyu 0,6 $’ı. Bunlara ek olarak uygulama her çağrıda markanın kalan aylık ve günlük bütçesini başlık olarak gönderiyor. Önizleme şeridinin kendi havuzu var; ödeme yapmamış kullanım ücretli şeridi tüketemiyor.
Akışlı Claude cevaplarında gövde tee() ile ikiye bölünüyor: bir kol kullanıcıya gidiyor, diğeri waitUntil içinde SSE olaylarından token sayısını okuyup defteri kapatıyor.
OpenAI önce, Claude yedek: hangi hatada sağlayıcı değişiyor?
Karar kayıtlarında K42 numarasıyla, 27 Ekim 2026 tarihli: her AI çağrısı önce OpenAI’a gidiyor, OpenAI çökerse, sınıra takılırsa ya da hata verirse Claude devralıyor. Kurucunun notu da kayıtta: “openai öncelik olsun, ama anthropic de backup olsun… hiçbir türlü patlamasın.” Gerekçe fiyat: aynı kayda göre milyon token başına gpt-6.1-sol 2/10 $, Opus 5.5 ise 4/20 $.
Geçiş kararı packages/ai/src/llm.ts içinde ve bilerek dar tutuldu. OpenAI tarafında bağlantı hatası ile 401, 402, 403, 408, 409, 429 ve 500–504 durumları yedeğe geçiriyor. Claude tarafında buna 529 ekleniyor; ayrıca bakiye bittiğinde Anthropic’in 402 yerine döndüğü 400 “credit balance is too low” mesajı da kesinti sayılıyor. Geçersiz istek ya da içerik reddi ise geçirmiyor, çünkü diğer sağlayıcıda da aynı şekilde başarısız olur.
Geçiş bir konuşmada yalnızca bir kez oluyor (switched bayrağı). Konuşmanın iki dökümü tutuluyor: Claude’un düşünme bloklarını koruyan, önbellek bozulmasın diye yalnızca sona eklenen bir döküm ve yedek modelin okuyabileceği düz metin döküm. Böylece onarım turunun ortasında sağlayıcı değişse bile model kaldığı yerden devam ediyor.
Yapılandırılmış çıktıda da iki geri dönüş var. OpenAI katı json_schema modunda 400 verirse aynı tur json_object moduyla tekrar soruluyor ve şema bir WeakSet’e yazılıyor. Claude “grammar is too large” derse şema sistem istemine taşınıyor; doğrulamayı zod ve onarım turu üstleniyor.
AI harcama izni neden tek bir modülde?
Gateway, bir çağrının ne kadar tutabileceğini biliyor ama kimin harcamaya hakkı olduğunu bilmiyor. Bu karar web tarafında, apps/web/src/server/spend-access.ts içinde. Dosyanın başındaki kural açık: bir müşteri ödediğinden fazlasına mal olamaz, plan almamış biri neredeyse hiçbir şeye mal olmaz.
spendAccess(planId, development, purpose) veritabanına dokunmayan saf bir fonksiyon ve 17 birim testi var. Sonuçlar şöyle:
- Etkin ücretli ya da iç paket
appşeridinde harcıyor. - Ücretsiz önizleme yalnızca
purpose: "preview"çağrısını yapabiliyor vetrialşeridine düşüyor; geri kalan her şeypreview_onlygerekçesiyle reddediliyor. - Paketi henüz alınmamış marka
pending_package, süresi dolmuş paketlapsed, bilinmeyen paketno_planile hiçbir şey harcamıyor.
Bütçe hesabı da aynı dosyada. Ücretli markanın aylık model bütçesi ödediği paket fiyatının ve o dönem aldığı eklentilerin %10’u. Günlük tavan bu bütçenin yarısı; en az 3 $, ama hiçbir zaman aylık bütçeden fazla değil. Böylece bir öğleden sonra yapılan yeniden çalıştırmalar ayın bütçesini bitiremiyor. Önizlemenin bütçesi 0,6 $ ve ömür boyu hiç dolmuyor.
Plan çalışma alanının değil markanın: her marka kendi paketi ve bütçesiyle geliyor. server/ai.ts içindeki assertSpendAccess her AI ya da medya harcayan sunucu fonksiyonunda ve Workflow adımında çağrılıyor, sonuç gateway’e dört başlık olarak gidiyor.
FeedHand neden 6 ayrı Cloudflare Workflow kullanıyor?
Web Worker’ının yapılandırmasında altı Workflow bağlı: brand-analysis, plan-build, production, market-research, competitor-research ve replacement. Hepsi src/server.ts içinden dışa aktarılıyor; TanStack Start HTTP’yi karşılıyor, arka plan işi Workflow sınıflarında.
Sebebi bir commit mesajında açık. Reddedilen bir gönderinin yerine yenisini yazan iş önceden waitUntil içinde çalışıyordu; o birkaç saniye içinde bir deploy ya da kesinti olursa iş sessizce kayboluyordu. Şimdi ReplacementWorkflow şu adımlardan oluşuyor:
undo-window: 5 saniyelik uyku, arayüzdeki 4,5 saniyelik geri alma süresi artı pay.check: gönderi hâlâ reddedilmiş ve en son sürüm mü?charge: bir düzenleme kredisi düşülüyor; aynı işlemde biraudit_logişareti yazılıyor ve Postgres advisory lock alınıyor. Commit edip ölen bir adım yeniden denendiğinde ikinci kez ücret almıyor.write: model çağrısı; 2 tekrar, üstel bekleme, 5 dakika zaman aşımı.refund: son deneme de başarısızsa kredi bir kez iade ediliyor.
Aynı mantık video üretiminde de var. Her klip kendi adımında gönderiliyor (submit-clip-N, 45 saniye aralıklı 12 tekrar), çünkü medya sağlayıcısı hesap başına aynı anda en çok 5 video çalıştırıyor ve 429 yük altında normal. İkinci klipten sonra yeniden başlayan bir çalışma üçüncü klipten devam ediyor; ilk ikisi tekrar gönderilmiyor.
Dayanıklı adımlar hangi tuzakları getiriyor?
Workflow’larda en çok dikkat isteyen kural steps.ts içinde yazılı: step.do dışındaki kod her yeniden oynatmada, yani her uyku, yeniden deneme ya da tahliye sonrasında tekrar çalışıyor. Bu yüzden ilerleme işaretleri yalnızca ileri gidiyor. advanceStep, “done” olmuş bir adımı tekrar “running”e çeviren ya da sayacını 30’dan 0’a düşüren bir güncellemeyi yok sayıyor. Hata ise her zaman yazılıyor, önceki bir “done”un arkasına saklanmıyor.
İkinci tuzak adım sonucunun boyutu. Bir adımın döndürdüğü değer 1 MiB sınırına takılabiliyor. Marka analizindeki collect adımı bu yüzden kaynak metinlerini değil yalnızca kimliklerini döndürüyor; metinler veritabanında kalıyor. Deploy öncesi başlamış ve sonra yeniden oynatılan eski çalışmalar için eski biçim de hâlâ okunuyor.
Üçüncüsü bağlantılar. Her adım kendi veritabanı istemcisini açıp adım bitince kapatıyor (withStepDb, en çok 2 bağlantı); önceden her sorgu yardımcısı kendi istemcisini açıyordu.
Plan üretiminde maliyet ile süre arasında bir denge var. Bir ay, 10 gönderilik partiler hâlinde yazılıyor ve bir parti asla iki pazarı karıştırmıyor. İlk parti tek başına çalışıp istem önbelleğini yazıyor, kalanlar 6’şar paralel adımda o önbelleği okuyor. Her yazma adımı 2 kez tekrar ediyor ve 10 dakikada zaman aşımına uğruyor. Strateji çağrısı başarısız olursa plan durmuyor; profilden çıkan deterministik strateji devralıyor.
`packages/rules` ve `ai-tells.ts`: model çıktısı yayına nasıl hazırlanıyor?
Modelin döndürdüğü her gönderi @feedhand/rules paketinden geçiyor: 27 kaynak dosya, yaklaşık 7.500 satır ve 19 test dosyası. Kurallar deterministik; model çağrısı yok.
ai-tells.ts (813 satır) bir metnin dil modeli tarafından yazıldığını belli eden 16 tür izi arıyor: kalıp açılışlar (“Discover…”, “Keşfedin”), “sadece bir X değil, bir Y” yapısı, üst üste üç sıfat, tek kelimelik kesik cümleler, hemen cevaplanan soru (“Sırrı mı? Sabır.”), ritim için em dash, emoji madde işaretleri, metin içi hashtag duvarı. Listeler İngilizce, Türkçe, Almanca, Fransızca ve İspanyolca için veri olarak tutuluyor. Her izin ağırlığı 1 ya da 2; toplam 2’ye ulaşınca metin yeniden yazıma gidiyor, tek zayıf iz yetmiyor.
Yanlış pozitif bir onarım turuna mal olduğu için dedektör temkinli: tırnak içi hiç okunmuyor, marka ve ürün adları önce siliniyor, sahibinin kendi gönderilerindeki alışkanlıklar iz sayılmıyor. Em dash ve hashtag duvarı gibi mekanik izler kodda, tur harcamadan düzeltiliyor. Kalanlar için yalnızca o gönderiler hedefli bir onarım turuna gidiyor; parti bu yüzden asla düşmüyor.
Platform sınırları da aynı pakette. limits.ts Instagram’da gönderi başına en çok 5 hashtag tutuyor; Graph API belgeleri hâlâ 30 dese de uygulama artık 5 kabul ediyor. X’te bağlantı varsayılan olarak kapalı, çünkü API’de URL’li gönderi 0,20 $, URL’siz 0,015 $. Reddit’te tanıtım gönderileri %10’un altında tutuluyor (9:1 kuralı).
Benzerini kuracaklar için kontrol listesi
FeedHand yayında ve ödemeleri açık. Aşağıdakiler kodda var ve testli; trafik altında ölçülmüş performans iddiası değil. Karar kayıtlarında da bir sınır not edilmiş: AI izi ölçümü gerçek modelle değil, deterministik taslak yazarın 600 gönderisi üzerinde yapıldı.
- Sağlayıcı anahtarlarını genel URL’si olmayan ayrı bir Worker’a koyun; uygulama ona yalnızca servis bağlamasıyla ve bir token’la ulaşsın.
- SDK’ları değiştirmeyin;
baseURLvefetchvermek yeterli. - Harcamayı çağrıdan önce en kötü tahminle ayırın, sonra gerçek maliyete kapatın. Atomiklik için tek bir Durable Object yeterli.
- Ücretsiz kullanıma ayrı bir şerit ve ayrı bir havuz verin.
- Sağlayıcı değişimini yalnızca kesinti sayılan durumlarda ve konuşma başına bir kez yapın; geçersiz isteği diğer sağlayıcıya taşımayın.
- Kimin harcayabileceğini veritabanından bağımsız tek bir saf fonksiyonda toplayın ve her kuralını test edin.
waitUntilile başlayan ve para harcayan her işi Workflow adımına taşıyın; ücret alan adımı idempotent yapın.step.dodışındaki kodun tekrar çalışacağını varsayın ve adım sonuçlarını küçük tutun.- Model çıktısını yayından önce deterministik kurallardan geçirin; mekanik hataları kodda düzeltip modele yalnızca gerçekten yeniden yazılması gerekeni geri gönderin.
AMG Yazılım olarak bu tür yapay zekâ destekli SaaS ürünlerini, kendi ürünlerimizde ve müşterilerimiz için geliştiriyoruz.
PROJENİZE UYARLAYALIM
Yapay Zekâ Entegrasyonu
Görüntü tanıma, doğal dil ve öneri sistemleri. Maliyeti ve belirsizliği gizlemeyen tasarım.
