LLM API gateway, uygulamalarınızla model sağlayıcısı arasında duran ve her isteği kendi kurallarınızdan geçiren ara katmandır. Sağlayıcı anahtarını tek yerde tutar, hangi müşterinin ya da iç ekibin (bu yazıda kiracı, İngilizcede tenant) hangi modeli ne kadar kullanabileceğine karar verir, yanıtı istemciye akış hâlinde iletir ve her isteğin maliyetini kayda geçirir. Kâğıt üzerinde basit bir ters proxy gibi görünür; zorluk, akışlı yanıtın ortasında gelen kullanım verisini doğru okumak ve yarıda kalan istekleri doğru faturalamaktır.
Bu yazı Anthropic Messages API üzerinden ilerliyor ve OpenAI’nin farklı davrandığı noktaları ayrıca gösteriyor. Örnek kodlar Node.js 20 ve sonrası için bağımlılıksız yazıldı; parçalanmış akış, kopan istemci, akış ortasında hata ve eşzamanlı bütçe rezervasyonu senaryolarıyla yerelde test edildi. Fiyat ve davranış bilgileri 10 Ekim 2026 tarihli sağlayıcı belgelerine dayanıyor.
Kısaca
- Sağlayıcıdan gelen SSE baytlarını değiştirmeden aktarın; kullanımı yalnız bir kopyadan ölçün.
- Anthropic’te
message_deltakullanım sayıları kümülatiftir: toplamayın, son değeri yazın. - Anthropic ve OpenAI’de
input_tokensfarklı şeyi sayar; sağlayıcınınusagenesnesini olduğu gibi saklayın. - Maliyeti sürümlü bir fiyat tablosuyla, tam sayı birimle ve token sınıflarına ayırarak hesaplayın.
- Bütçeyi istekten önce bloke edin; yarım kalan isteği 0 değil kesinleşmemiş sayın ve sağlayıcı raporuyla mutabakat yapın.
Gateway ne işe yarar?
Uygulamalar sağlayıcıya doğrudan bağlandığında anahtar her serviste çoğalır, maliyet hangi ürüne ya da müşteriye ait bilinmez ve model değişikliği her ekipte ayrı ayrı yapılır. Gateway bu sorumlulukları tek noktada toplar:
- Anahtar izolasyonu: Sağlayıcı anahtarı yalnız gateway’de bulunur. Uygulamalar ve kiracılar kendi anahtarlarıyla gateway’e bağlanır; bu anahtarlar tek tek iptal edilebilir.
- Politika: Hangi kiracının hangi modeli kullanabileceği, istek başına en fazla kaç çıktı token’ı üretilebileceği ve hangi beta özelliklerinin açık olduğu burada belirlenir.
- Bütçe: İstek sağlayıcıya gitmeden önce kiracının bütçesinden pay ayrılır; harcama sınırı istek sonuçlandıktan sonra değil, önce uygulanır.
- Ölçüm ve maliyet atfı: Her isteğin girdi, önbellek ve çıktı kullanımı ayrı ayrı kaydedilir ve kiracıya, ürüne ya da özelliğe bağlanır.
- Gözlemlenebilirlik: İlk token süresi, hata türleri ve önbellek oranı tek yerden izlenir.
Gateway’in yapmaması gereken işler de aynı derecede önemlidir. Akıştaki olayları yeniden yazmamalı, tanımadığı olayları atmamalı ve istem (prompt) içeriğini varsayılan olarak saklamamalıdır. Bu üçü, sağlayıcı yeni bir özellik eklediğinde sessiz kırılmalara ve kişisel veri sorumluluğuna yol açar.
Bir isteğin yaşam döngüsü
Akışlı bir isteğin gateway içindeki yolu sekiz adımdan oluşur:
- Kimlik doğrulama: Kiracı anahtarı doğrulanır, istek gövdesi boyut sınırıyla okunur.
- Politika: Model izni,
max_tokenstavanı ve izin verilen başlıklar uygulanır. - Rezervasyon: İsteğin en kötü durumdaki maliyeti hesaplanır ve kiracının bütçesinden ayrılır. Bütçe yetmiyorsa istek burada reddedilir.
- Upstream isteği: İstek, gateway’in sağlayıcı anahtarıyla ve yalnız izin verilen başlıklarla sağlayıcıya gönderilir.
- Aktarım: Sağlayıcıdan gelen baytlar değiştirilmeden istemciye yazılır.
- Ölçüm: Aynı baytların bir kopyası ayrıştırılır ve kullanım bilgisi toplanır.
- Sonuçlandırma: Akış tamamlandıysa kesin maliyet yazılır ve rezervasyon kapatılır. Tamamlanmadıysa kayıt “kesinleşmedi” olarak işaretlenir.
- Mutabakat: Gateway kayıtları düzenli olarak sağlayıcının kullanım ve maliyet raporlarıyla karşılaştırılır.
Rezervasyon ve sonuçlandırma iki ayrı, kısa veritabanı işlemidir. Akış dakikalarca sürebilir; bu süre boyunca açık bir veritabanı transaction’ı ve satır kilidi tutmak aynı kiracının diğer isteklerini bekletir.
Akışı bozmadan aktarmak
Gateway’in istemciye yazdığı her bayt, sağlayıcının gönderdiği baytla aynı olmalıdır. Olayları ayrıştırıp yeniden serileştirmek üç sorun çıkarır: alan sırası ve boşluklar değişir, ara sıra gelen ping olayları kaybolabilir ve sağlayıcının sonradan eklediği olay türleri istemciye ulaşmaz. Anthropic, sürümleme politikası gereği yeni olay türleri eklenebileceğini ve istemcilerin tanımadıkları olayları sorunsuz geçmesi gerektiğini belgesinde açıkça yazar. Doğru yaklaşım, baytları olduğu gibi aktarmak ve ölçüm için yalnız bir kopyayı ayrıştırmaktır.
İstek başlıkları: Sağlayıcıya giden başlıklar izin listesiyle kurulur. İstemcinin Authorization başlığı, çerezleri ve rastgele başlıkları upstream’e taşınmaz; beta özellikleri açan anthropic-beta değerleri yalnız onaylanmışsa geçer. RFC 9110, Connection, Keep-Alive, TE, Transfer-Encoding ve Upgrade gibi bağlantıya özgü (hop-by-hop) alanların ara katmanda kaldırılması ya da değiştirilmesi gerektiğini söyler.
Yanıt başlıkları: Sağlayıcı yanıtındaki anthropic-ratelimit-* ve anthropic-organization-id başlıkları sağlayıcı hesabınıza aittir. Bunları kiracıya aktarmak, kuruluşunuzun toplam kapasitesini ve hesap kimliğini dışarı açar. Kiracıya gateway’in kendi istek kimliğini ve kendi limit bilgisini dönün; sağlayıcının request-id değerini ise destek talepleri için kendi kaydınızda saklayın.
Tamponlama ve sıkıştırma: Yanıt text/event-stream olarak, Cache-Control: no-cache ve nginx için X-Accel-Buffering: no başlıklarıyla gönderilir. Akış rotası sıkıştırma ara katmanından muaf tutulmalıdır. Bu ayarların ayrıntısı Server-Sent Events yazısındaki proxy ve tamponlama bölümünde anlatılıyor.
Zaman aşımı: Akışlı istekte toplam süre sınırı yanlış ölçüdür; uzun bir yanıt meşru olarak dakikalar sürebilir. Bunun yerine iki parça arasında geçen süre sınırlanır. Anthropic akışına ping olayları girebilir, ancak sıklıkları belgelenmemiştir ve model çalışırken olaylar arasında gecikme olabileceği de belirtilir. Bu yüzden boşta kalma süresi cömert seçilmeli ve gerçek trafikte görülen en uzun sessizlik ölçülerek ayarlanmalıdır. Önündeki nginx’in proxy_read_timeout değeri de bu süreden uzun olmalıdır.
Geri basınç: İstemci yavaş okuyorsa res.write() çağrısı false döner. Gateway bu durumda drain olayını beklemeden yazmaya devam ederse yanıt bellekte birikir.
SSE akışını ölçüm için ayrıştırmak
Ağdan gelen parçalar olay sınırlarıyla hizalı değildir. Bir parça olayın ortasında, satırın ortasında hatta bir karakterin ortasında bitebilir. WHATWG spesifikasyonu ayrıştırıcının uyması gereken kuralları tanımlar:
- Satırlar
CRLF, tekLFya da tekCRile ayrılabilir. - Akış UTF-8 olarak çözülür ve baştaki BOM atılır.
- Boş satır olayı bitirir. Akış boş satır gelmeden kapanırsa yarım kalan son olay teslim edilmez.
:ile başlayan satırlar yorumdur.
UTF-8’de ğ, ş ve İ gibi karakterler iki bayt tutar. Parça sınırı bu iki baytın arasına düşerse her parçayı ayrı ayrı çözen kod bozuk karakter üretir. TextDecoder ile { stream: true } kullanmak tamamlanmamış baytı bir sonraki parçaya kadar bekletir. Aynı sorun \r\n çiftinin iki parçaya bölünmesinde de vardır.
// sse-parser.js: Bayt parçalarını alır, tamamlanan SSE olaylarını döndürür.
// Akışı değiştirmez; gateway aynı baytları istemciye ayrıca aktarır.
export class SseParser {
#decoder = new TextDecoder("utf-8"); // baştaki BOM'u kendisi atar
#buffer = "";
#eventType = "";
#dataLines = [];
push(chunk) {
// stream: true, parça sınırında bölünen çok baytlı karakteri (ğ, ş, İ) bekletir
this.#buffer += this.#decoder.decode(chunk, { stream: true });
return this.#drain(false);
}
end() {
this.#buffer += this.#decoder.decode();
const events = this.#drain(true);
// Spesifikasyon: boş satırla bitmeyen son olay teslim edilmez
this.#eventType = "";
this.#dataLines = [];
return events;
}
#drain(final) {
const events = [];
let index;
while ((index = this.#buffer.search(/[\r\n]/)) !== -1) {
// "\r\n" iki parçaya bölünmüş olabilir: tampon "\r" ile bitiyorsa bekle
const lastIsCr = index === this.#buffer.length - 1;
if (this.#buffer[index] === "\r" && lastIsCr && !final) break;
const width = this.#buffer.startsWith("\r\n", index) ? 2 : 1;
const line = this.#buffer.slice(0, index);
this.#buffer = this.#buffer.slice(index + width);
const event = this.#line(line);
if (event) events.push(event);
}
return events;
}
#line(line) {
if (line === "") {
// Boş satır olayı bitirir; data satırı yoksa teslim edilecek olay da yoktur
const event = this.#dataLines.length
? {
type: this.#eventType || "message",
data: this.#dataLines.join("\n"),
}
: null;
this.#eventType = "";
this.#dataLines = [];
return event;
}
if (line.startsWith(":")) return null; // yorum satırı (heartbeat)
const colon = line.indexOf(":");
const field = colon === -1 ? line : line.slice(0, colon);
let value = colon === -1 ? "" : line.slice(colon + 1);
if (value.startsWith(" ")) value = value.slice(1);
if (field === "event") this.#eventType = value;
else if (field === "data") this.#dataLines.push(value);
// id ve retry alanları kullanım ölçümü için gerekmiyor
return null;
}
}
Anthropic akışında kullanım bilgisi
Messages API akışı sabit bir sırayla ilerler: message_start, her içerik bloğu için content_block_start, çoğunlukla bir veya daha fazla content_block_delta ve content_block_stop, ardından bir veya daha fazla message_delta ve son olarak message_stop. Araya herhangi sayıda ping olayı girebilir. Web araması sonucu gibi bazı bloklar ise yalnız başlangıç ve bitiş olayıyla gelir.
Kullanım bilgisi iki yerde gelir:
message_start:message.usagenesnesi girdi token’larını, önbelleğe yazılan ve önbellekten okunan token’ları ve başlangıçtaki küçük bir çıktı sayısını taşır.message_delta:usagealanındaki sayılar kümülatiftir. Çıktı sayısı hermessage_deltaolayında o ana kadarki toplamı gösterir.
Kümülatif sayıları toplamak en sık yapılan hatadır; iki message_delta olayı gelen bir akışta çıktı iki kez sayılır. Doğrusu, her message_delta olayındaki alanları önceki değerlerin üzerine yazmaktır. Bu kural yalnız çıktı için de geçerli değildir. Anthropic’in web araması örneğinde message_start içinde 2.679 olan input_tokens, sunucu tarafı aracın getirdiği sonuçlarla birlikte son message_delta içinde 10.682 olarak gelir ve aynı olay server_tool_use.web_search_requests alanını taşır. message_start değerini kesin girdi sayısı kabul eden kod bu isteği eksik faturalar.
Akış 200 OK ile başladıktan sonra da hata gelebilir. Yoğun dönemlerde sağlayıcı akışın içinde error olayı gönderir; örneğin overloaded_error, akışsız bir istekte HTTP 529’a karşılık gelir. Bu nedenle HTTP durum kodu tek başına başarı ölçüsü değildir: isteğin tamamlandığını yalnız message_stop olayı gösterir.
// anthropic-usage.js: Messages API akışından kullanım ve sonuç bilgisini toplar.
export function createUsageTracker() {
const state = {
model: null,
usage: null, // sağlayıcının usage nesnesi, alan adları değiştirilmeden
stopReason: null,
completed: false, // message_stop görüldü mü
error: null, // akış içinde gelen error olayı
};
function onEvent(event) {
let payload;
try {
payload = JSON.parse(event.data);
} catch {
return; // ölçümü ilgilendirmeyen, JSON olmayan olay
}
switch (payload.type) {
case "message_start":
state.model = payload.message.model;
state.usage = { ...payload.message.usage };
break;
case "message_delta":
// message_delta içindeki sayılar kümülatiftir: toplanmaz, üzerine yazılır
for (const [key, value] of Object.entries(payload.usage ?? {})) {
if (value != null) state.usage = { ...state.usage, [key]: value };
}
state.stopReason = payload.delta?.stop_reason ?? state.stopReason;
break;
case "message_stop":
state.completed = true;
break;
case "error":
state.error = payload.error;
break;
default:
// ping, içerik olayları ve ileride eklenecek yeni olay türleri
break;
}
}
return { state, onEvent };
}
OpenAI’de kullanım bilgisi farklı gelir
Birden fazla sağlayıcıyı aynı gateway’den geçirecekseniz kullanım alanlarının aynı adı taşıyıp farklı anlam taşıdığı noktalara dikkat etmek gerekir.
Chat Completions: Akışlı istekte kullanım bilgisi varsayılan olarak gelmez. stream_options: { include_usage: true } verildiğinde data: [DONE] satırından önce ek bir parça gönderilir; bu parçanın usage alanı isteğin tamamının kullanımını taşır ve choices dizisi her zaman boştur. Diğer parçalarda usage alanı null değerindedir. OpenAI’nin API başvuru belgesi, akış kesilirse bu son parçanın hiç gelmeyebileceğini ayrıca not eder. choices[0] değerini kontrol etmeden okuyan kod da bu son parçada hata verir.
Responses API: Akış response.completed olayıyla biter ve kullanım bilgisi bu olaydaki response.usage nesnesinde gelir. Başarısız ya da yarım kalan yanıtlar için response.failed ve response.incomplete, akış sırasında oluşan hatalar için error olayı vardır; ölçüm kodu bu dört sonlanma yolunu da ele almalıdır.
En önemli fark girdi sayısının tanımıdır. Anthropic’te input_tokens yalnız son önbellek kırılma noktasından sonraki token’ları gösterir; önbellekten okunan ve önbelleğe yazılan token’lar ayrı alanlarda gelir ve toplam girdi üçünün toplamıdır. OpenAI’nin prompt caching belgesindeki maliyet örneği ise önbelleksiz girdiyi input_tokens değerinden cached_tokens ve cache_write_tokens çıkarılarak hesaplar; yani önbellek sayıları input_tokens içinde yer alır.
| Kullanım | Anthropic Messages | OpenAI Responses |
|---|---|---|
| Önbellekten okunan girdi | cache_read_input_tokens (ayrı alan) | input_tokens_details.cached_tokens (toplamın içinde) |
| Önbelleğe yazılan girdi | cache_creation_input_tokens (ayrı alan) | input_tokens_details.cache_write_tokens (toplamın içinde) |
| Önbelleksiz girdi | input_tokens | input_tokens eksi iki önbellek alanı |
| Toplam girdi | Üç alanın toplamı | input_tokens |
İki sağlayıcının input_tokens alanını aynı sütuna yazan gateway, Anthropic isteklerinde girdiyi eksik, OpenAI isteklerinde önbellek indirimini yanlış hesaplar. OpenTelemetry’nin üretken yapay zekâ semantik kuralları da (henüz Development aşamasında) Anthropic için gen_ai.usage.input_tokens değerinin input_tokens, önbellekten okunan ve önbelleğe yazılan token’ların toplamı olarak hesaplanmasını şart koşar. Güvenli yol, sağlayıcının usage nesnesini hiç değiştirmeden saklamak ve normalleştirilmiş sütunları bunun yanında, sağlayıcıya özgü kurallarla üretmektir.
Claude API maliyeti nasıl hesaplanır?
Bir isteğin maliyeti “token sayısı çarpı fiyat” değildir. 10 Ekim 2026 itibarıyla Anthropic fiyatlandırmasında aynı istek içinde birden fazla fiyat sınıfı bulunur:
- Önbelleğe yazma: 5 dakikalık önbellek için taban girdi fiyatının 1,25 katı, 1 saatlik önbellek için 2 katı.
- Önbellekten okuma: Çoğu modelde taban girdi fiyatının 0,1 katı. Claude Opus 5.5 ve Sonnet 5.5’te 0,05 katı, Claude Fable 5.1 ve Mythos 5.1’de 0,025 katı.
- Uzun istem kademesi: Claude Haiku 5.5, istemi 100.000 token’ı aşan isteklerde daha yüksek fiyat uygular. Bu uzunluğa önbellekten okunan ve önbelleğe yazılan token’lar da dahildir.
- Veri yerleşimi: Claude 4.6 ve sonraki modellerde
inference_geo: "us"ile yalnız ABD’de çıkarım istemek tüm token sınıflarına 1,1 çarpanı uygular. - Toplu işleme: Batch API girdi ve çıktıda yüzde 50 indirim sağlar.
- Sunucu araçları: Web araması 1.000 arama başına 10 dolar ücretlendirilir; hata veren arama ücretlendirilmez. Arama sonuçları ayrıca girdi token’ı olarak sayılır.
Bu yüzden fiyatlar kodun içine sabit olarak gömülmez, yürürlük tarihi olan sürümlü veri olarak tutulur ve her maliyet kaydına hangi fiyat sürümüyle hesaplandığı yazılır. Para birimi kesirli sayıyla tutulmaz; aşağıdaki örnek token başına nano-USD (1 USD = 10⁹) kullanır, böylece $0,10 / MTok gibi fiyatlar da tam sayı olarak ifade edilir.
Anthropic’in prompt caching belgesindeki örnek kullanımı Claude Sonnet 5.5 fiyatlarıyla hesaplayalım. Fiyatlar sık değişir; tablo 10 Ekim 2026 tarihlidir ve kendi hesabınızda güncel fiyat sayfası esas alınmalıdır.
| Sınıf | Token | Fiyat (USD / MTok) | Maliyet (USD) |
|---|---|---|---|
| Önbelleksiz girdi | 2.048 | 2,00 | 0,004096 |
| Önbellekten okuma | 1.800 | 0,10 | 0,000180 |
| Önbelleğe yazma (5 dakika) | 148 | 2,50 | 0,000370 |
| Önbelleğe yazma (1 saat) | 100 | 4,00 | 0,000400 |
| Çıktı | 503 | 10,00 | 0,005030 |
| Toplam | 0,010076 |
Önbellek okumasını taban girdi fiyatıyla, önbellek yazımını da indirimsiz girdi fiyatıyla hesaplayan bir gateway bu istekte farklı bir tutar bulur ve hata trafikle birlikte büyür.
// cost.js: Fiyatlar kodda sabit değil, sürümlü veri olarak tutulur.
// Birim: token başına nano-USD (1 USD = 1e9). $2 / MTok = 2000 nano-USD / token.
export const PRICE_TABLE = {
version: "anthropic-2026-10-10",
models: {
"claude-sonnet-5-5": {
input: 2000,
cacheWrite5m: 2500,
cacheWrite1h: 4000,
cacheRead: 100,
output: 10000,
webSearch: 10_000_000, // arama başına $0,01 ($10 / 1.000 arama)
},
},
};
export class UsageIncompleteError extends Error {}
export function costNanoUsd(usage, price) {
// Zorunlu alanlar yoksa maliyet bilinmiyordur; 0 kabul edilmez
if (
!Number.isInteger(usage?.input_tokens) ||
!Number.isInteger(usage?.output_tokens)
) {
throw new UsageIncompleteError("usage eksik; kayıt mutabakata bırakılmalı");
}
// Cache alanları önbellek kullanılmayan isteklerde 0 gelir
const cacheRead = usage.cache_read_input_tokens ?? 0;
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
const write1h = usage.cache_creation?.ephemeral_1h_input_tokens ?? 0;
const write5m =
usage.cache_creation?.ephemeral_5m_input_tokens ?? cacheWrite - write1h;
const searches = usage.server_tool_use?.web_search_requests ?? 0;
return (
usage.input_tokens * price.input +
write5m * price.cacheWrite5m +
write1h * price.cacheWrite1h +
cacheRead * price.cacheRead +
usage.output_tokens * price.output +
searches * price.webSearch
);
}
Örnek fonksiyon Haiku 5.5’in uzun istem kademesini ve inference_geo çarpanını uygulamaz. Bu modelleri ya da bölge seçimini kullanıyorsanız fiyat seçimi isteğin toplam girdi uzunluğuna ve istek parametrelerine bakmalıdır.
Bütçe: önce rezervasyon, sonra kesinleştirme
Harcamayı istek bittikten sonra kaydetmek, bütçe sınırının ancak aşıldıktan sonra fark edilmesi demektir. Ödeme sistemlerindeki provizyon mantığı burada da işe yarar: istekten önce en kötü durumdaki tutar bloke edilir, istek bitince gerçek tutar kesinleşir ve fark serbest kalır.
En kötü durum, tüm girdinin en pahalı girdi sınıfından (1 saatlik önbelleğe yazma) ve çıktının max_tokens sınırına kadar faturalanmasıdır. Girdi sayısı için Anthropic’in token sayma uç noktası kullanılabilir. Bu uç nokta ücretsizdir, mesaj oluşturmadan bağımsız bir istek sınırına tabidir ve döndürdüğü sayı bir tahmindir. Web araması gibi sunucu araçları içeren istekleri ise kabul etmez; bu durumda isteği bütçe kontrolü olmadan geçirmek yerine reddetmek ya da kendi muhafazakâr tahmininizi kullanmak gerekir. Sunucu araçlarının getirdiği sonuçlar da girdiye eklendiği için bu araçlar açıkken rezervasyon kesin bir üst sınır değildir; araç kullanım sayısını ayrıca sınırlayın.
Rezervasyon tek bir SQL ifadesiyle yapılır: bütçe kontrolü ve bloke etme aynı UPDATE içinde olduğu için aynı anda gelen istekler sınırı aşamaz. Aşağıdaki tablo ve sorgular PostgreSQL 15’te, aynı kiracıya eşzamanlı 30 rezervasyon gönderilerek denendi; 100 birimlik bütçeye 10’ar birimlik isteklerden tam 10 tanesi geçti.
CREATE TABLE tenant_budget (
tenant_id text PRIMARY KEY,
limit_nanousd bigint NOT NULL CHECK (limit_nanousd >= 0),
spent_nanousd bigint NOT NULL DEFAULT 0 CHECK (spent_nanousd >= 0), -- kesinleşmiş maliyet
held_nanousd bigint NOT NULL DEFAULT 0 CHECK (held_nanousd >= 0) -- açık rezervasyonlar
);
CREATE TABLE llm_usage_ledger (
request_id uuid PRIMARY KEY, -- gateway'in kendi istek kimliği
tenant_id text NOT NULL REFERENCES tenant_budget (tenant_id),
model text NOT NULL,
status text NOT NULL
CHECK (status IN ('reserved', 'settled', 'unreconciled', 'released')),
reserved_nanousd bigint NOT NULL,
cost_nanousd bigint, -- kesinleşene kadar NULL; bilinmeyen maliyet 0 yazılmaz
lower_bound_nanousd bigint, -- yarım akışta bilinen alt sınır
usage_raw jsonb, -- sağlayıcının usage nesnesi, değiştirilmeden
price_version text,
upstream_request_id text,
reason text,
created_at timestamptz NOT NULL DEFAULT now(),
finalized_at timestamptz
);
CREATE INDEX llm_usage_ledger_tenant_created ON llm_usage_ledger (tenant_id, created_at);
CREATE INDEX llm_usage_ledger_open ON llm_usage_ledger (created_at)
WHERE status IN ('reserved', 'unreconciled');
-- Rezervasyon: $1 request_id, $2 tenant_id, $3 model, $4 tutar.
-- Etkilenen satır yoksa bütçe yetersizdir.
WITH hold AS (
UPDATE tenant_budget
SET held_nanousd = held_nanousd + $4
WHERE tenant_id = $2
AND spent_nanousd + held_nanousd + $4 <= limit_nanousd
RETURNING tenant_id
)
INSERT INTO llm_usage_ledger (request_id, tenant_id, model, status, reserved_nanousd)
SELECT $1, tenant_id, $3, 'reserved', $4 FROM hold;
-- Kesinleştirme: $1 request_id, $2 maliyet, $3 usage, $4 fiyat sürümü, $5 upstream request-id.
-- Yalnız açık kayıtları günceller; aynı isteği ikinci kez kesinleştirmek etkisizdir.
WITH done AS (
UPDATE llm_usage_ledger
SET status = 'settled', cost_nanousd = $2, usage_raw = $3,
price_version = $4, upstream_request_id = $5, finalized_at = now()
WHERE request_id = $1 AND status IN ('reserved', 'unreconciled')
RETURNING tenant_id, reserved_nanousd, cost_nanousd
)
UPDATE tenant_budget AS b
SET held_nanousd = b.held_nanousd - done.reserved_nanousd,
spent_nanousd = b.spent_nanousd + done.cost_nanousd
FROM done
WHERE b.tenant_id = done.tenant_id;
Kayıtların dört durumu vardır:
reserved: Bütçeden pay ayrıldı, istek sürüyor.settled: Kesin maliyet yazıldı, bloke edilen tutar bırakıldı.released: Sağlayıcı isteği akış başlamadan reddetti; yanıtta kullanım bilgisi olmadığı için gateway maliyet yazmaz ve bloke edilen tutarı geri bırakır.unreconciled: Akış tamamlanmadı ya da kullanım bilgisi eksik geldi. Bilinen alt sınır kaydedilir ama bloke edilen tutar, mutabakat kesin maliyeti bulana kadar serbest bırakılmaz.
Son durumdaki kayda 0 yazmak en tehlikeli kısayoldur: teknik olarak üretilmiş ve sağlayıcının faturalayacağı bir isteği maliyetsiz gösterir ve kiracının bütçesini olduğundan geniş gösterir.
Gateway iskeleti
Aşağıdaki kod önceki parçaları bir araya getirir. authenticate, countTokens ve ledger dışarıdan verilir: ilki kiracı anahtarını doğrular, ikincisi token sayma uç noktasını çağırır, üçüncüsü yukarıdaki SQL’i çalıştırır (reserve, rezervasyon sorgusu bir satır eklediyse true döner). Örnek yalnız akışlı istekleri kabul eder; akışsız yanıtta kullanım bilgisi tek bir JSON gövdesinde gelir.
Kodun kalbi akış döngüsüdür: her parça önce istemciye yazılır, sonra aynı parça ayrıştırıcıya verilir; akış bitince kayıt sonuçlandırılır. Kimlik, politika, rezervasyon, yeniden deneme ve hata eşleme dahil tam dosya hemen altındaki açılır blokta.
// gateway.js içinden: akış döngüsü ve sonuçlandırma
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"x-accel-buffering": "no", // nginx bu yanıtı tamponlamasın
});
const parser = new SseParser();
const tracker = createUsageTracker();
let transportError = null;
let idleTimedOut = false;
let idleTimer;
const armIdleTimer = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
idleTimedOut = true; // ping dahil hiçbir bayt gelmedi
upstreamAbort.abort();
}, idleTimeoutMs);
};
try {
armIdleTimer();
for await (const chunk of upstream.body) {
armIdleTimer();
// Baytlar olduğu gibi aktarılır; ayrıştırma yalnız ölçüm içindir
if (!res.write(chunk)) {
await once(res, "drain", { signal: upstreamAbort.signal });
}
for (const event of parser.push(chunk)) tracker.onEvent(event);
}
for (const event of parser.end()) tracker.onEvent(event);
} catch (error) {
transportError = error; // istemci koptu, upstream sustu ya da bağlantı kesildi
} finally {
clearTimeout(idleTimer);
res.end();
}
await finalize({
ledger,
price,
tracker,
transportError,
idleTimedOut,
record: {
requestId,
upstreamRequestId,
priceVersion: PRICE_TABLE.version,
},
});
Tam kod: gateway.js
// gateway.js: Node.js 20+ (yerleşik fetch), bağımlılık yok.
import { randomUUID } from "node:crypto";
import { once } from "node:events";
import { SseParser } from "./sse-parser.js";
import { createUsageTracker } from "./anthropic-usage.js";
import { costNanoUsd, PRICE_TABLE } from "./cost.js";
const MAX_BODY_BYTES = 32 * 1024 * 1024; // Messages API istek sınırı
const IDLE_TIMEOUT_MS = 90_000; // iki parça arasında beklenecek en uzun süre
const MAX_RETRY_AFTER_S = 5; // daha uzun bekleme istenirse kiracıya 503 dön
const ALLOWED_BETAS = new Set(); // izin verilen anthropic-beta değerleri
class BodyTooLargeError extends Error {}
export function createGateway(options) {
const handle = createHandler(options);
return (req, res) =>
handle(req, res).catch(error => {
// Yakalanmayan hata süreci düşürmesin
console.error("gateway_error", error);
if (!res.headersSent) sendError(res, 500, "gateway_error");
else res.destroy();
});
}
function createHandler({
upstreamUrl,
apiKey,
authenticate,
countTokens,
ledger,
idleTimeoutMs = IDLE_TIMEOUT_MS,
}) {
return async function handle(req, res) {
const requestId = randomUUID();
res.setHeader("x-request-id", requestId);
// İstemci ayrılırsa üretimi sürdürmemek için upstream isteği iptal edilir.
// Dinleyici ilk await'ten önce kurulur; aksi halde erken kopma kaçırılır.
const upstreamAbort = new AbortController();
res.on("close", () => {
if (!res.writableFinished) upstreamAbort.abort();
});
const tenant = await authenticate(req); // kiracının anahtarı, sağlayıcınınki değil
if (!tenant) return sendError(res, 401, "authentication_error");
let body;
try {
body = JSON.parse(await readBody(req, MAX_BODY_BYTES));
} catch (error) {
if (error instanceof BodyTooLargeError) {
return sendError(res, 413, "request_too_large");
}
return sendError(res, 400, "invalid_request_error");
}
// Politika: model izni, çıktı tavanı ve yalnız akışlı istek
const price = PRICE_TABLE.models[body.model];
if (!price || !tenant.models.includes(body.model)) {
return sendError(res, 403, "model_not_allowed");
}
if (body.stream !== true) return sendError(res, 400, "stream_required");
const requested = body.max_tokens ?? tenant.maxTokens;
if (!Number.isInteger(requested) || requested < 1) {
return sendError(res, 400, "invalid_max_tokens");
}
body.max_tokens = Math.min(requested, tenant.maxTokens);
// En kötü durum: tüm girdi en pahalı girdi sınıfından, çıktı max_tokens'a kadar
let inputEstimate;
try {
inputEstimate = await countTokens(body);
} catch {
// Bütçe kontrolü yapılamıyorsa istek geçirilmez
return sendError(res, 503, "budget_check_unavailable");
}
const reserve =
inputEstimate * price.cacheWrite1h + body.max_tokens * price.output;
const reserved = await ledger.reserve({
requestId,
tenantId: tenant.id,
model: body.model,
reserve,
});
if (!reserved) return sendError(res, 402, "budget_exceeded");
let upstream;
try {
upstream = await fetchWithRetry(upstreamUrl, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
...pickBetaHeader(req.headers["anthropic-beta"]),
},
body: JSON.stringify(body),
signal: upstreamAbort.signal,
});
} catch {
await ledger.release({ requestId });
return sendError(res, 502, "upstream_unavailable");
}
const upstreamRequestId = upstream.headers.get("request-id");
if (upstream.status !== 200) {
// Akış başlamadı, usage dönmedi: rezervasyon geri bırakılır
await ledger.release({ requestId, upstreamRequestId });
const errorBody = await upstream.text();
if (isRequestError(upstream.status, errorBody)) {
// İsteğin kendisinden kaynaklanan hata kiracıya aynen döner
res.writeHead(upstream.status, { "content-type": "application/json" });
return res.end(errorBody);
}
// Kimlik, bakiye, harcama sınırı ve kapasite hataları sağlayıcı hesabına aittir
const busy = upstream.status === 429 || upstream.status === 529;
const retryAfter = upstream.headers.get("retry-after");
if (busy && retryAfter) res.setHeader("retry-after", retryAfter);
return sendError(res, busy ? 503 : 502, "upstream_unavailable");
}
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
"x-accel-buffering": "no", // nginx bu yanıtı tamponlamasın
});
const parser = new SseParser();
const tracker = createUsageTracker();
let transportError = null;
let idleTimedOut = false;
let idleTimer;
const armIdleTimer = () => {
clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
idleTimedOut = true; // ping dahil hiçbir bayt gelmedi
upstreamAbort.abort();
}, idleTimeoutMs);
};
try {
armIdleTimer();
for await (const chunk of upstream.body) {
armIdleTimer();
// Baytlar olduğu gibi aktarılır; ayrıştırma yalnız ölçüm içindir
if (!res.write(chunk)) {
await once(res, "drain", { signal: upstreamAbort.signal });
}
for (const event of parser.push(chunk)) tracker.onEvent(event);
}
for (const event of parser.end()) tracker.onEvent(event);
} catch (error) {
transportError = error; // istemci koptu, upstream sustu ya da bağlantı kesildi
} finally {
clearTimeout(idleTimer);
res.end();
}
await finalize({
ledger,
price,
tracker,
transportError,
idleTimedOut,
record: {
requestId,
upstreamRequestId,
priceVersion: PRICE_TABLE.version,
},
});
};
}
async function finalize({
ledger,
price,
tracker,
transportError,
idleTimedOut,
record,
}) {
const { usage, completed, error, stopReason } = tracker.state;
const base = { ...record, usage, stopReason };
let cost = null;
try {
cost = costNanoUsd(usage, price);
} catch {
cost = null; // usage eksik: maliyet bilinmiyor
}
try {
if (completed && !error && !transportError && cost !== null) {
return await ledger.settle({ ...base, costNanoUsd: cost });
}
// Kesinleşmeyen kayıt: elimizdeki sayı yalnız alt sınırdır
let reason = "missing_message_stop";
if (error) reason = error.type;
else if (idleTimedOut) reason = "upstream_idle";
else if (transportError) reason = "stream_interrupted";
else if (cost === null) reason = "usage_incomplete";
return await ledger.markUnreconciled({
...base,
lowerBoundNanoUsd: cost,
reason,
});
} catch (ledgerError) {
// Yanıt çoktan gönderildi; kayıt kaybolmasın diye kalıcı bir kuyruğa yazılmalı
console.error("ledger_write_failed", JSON.stringify(base), ledgerError);
}
}
// Yalnız ilk bayt aktarılmadan önce ve geçici hatalarda yeniden dene
async function fetchWithRetry(url, init, attempts = 3) {
for (let attempt = 1; ; attempt++) {
const response = await fetch(url, init);
const retryAfter = Number(response.headers.get("retry-after") ?? 0);
const retryable =
response.status >= 500 || (response.status === 429 && retryAfter > 0);
if (!retryable || attempt === attempts || retryAfter > MAX_RETRY_AFTER_S) {
return response;
}
await response.body?.cancel();
const backoffMs = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 250;
await sleep(backoffMs + Math.random() * 250, init.signal);
}
}
// 413 ve doğrulama kaynaklı 400 isteğin kendisine aittir. Anthropic, sizin
// tanımladığınız harcama sınırına ulaşıldığında da 400 döner; bu hesap durumudur.
function isRequestError(status, text) {
if (status === 413) return true;
if (status !== 400) return false;
try {
const message = JSON.parse(text).error?.message ?? "";
return !message.startsWith("You have reached your specified");
} catch {
return false;
}
}
function pickBetaHeader(value) {
const allowed = String(value ?? "")
.split(",")
.map(item => item.trim())
.filter(item => ALLOWED_BETAS.has(item));
return allowed.length ? { "anthropic-beta": allowed.join(",") } : {};
}
async function readBody(req, limit) {
const chunks = [];
let size = 0;
for await (const chunk of req) {
size += chunk.length;
if (size > limit) throw new BodyTooLargeError();
chunks.push(chunk);
}
return Buffer.concat(chunks).toString("utf8");
}
function sendError(res, status, type) {
res.writeHead(status, { "content-type": "application/json" });
res.end(JSON.stringify({ type: "error", error: { type } }));
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const timer = setTimeout(resolve, ms);
signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
reject(signal.reason);
},
{ once: true }
);
});
}
Kodda üç karar özellikle bilinçlidir:
- Yeniden deneme yalnız ilk bayttan önce: İstemciye bayt yazıldıktan sonra isteği sessizce tekrarlamak, kullanıcının aynı metnin başını iki kez görmesi ve iki kez faturalanması demektir. Bağlantı hatası, 5xx, 529 ve
retry-aftertaşıyan 429 yanıtları akış başlamadan yeniden denenir. Anthropic’in resmî SDK’sı da bu tür geçici hataları varsayılan olarak iki kez, üstel geri çekilmeyle veretry-afterbaşlığına uyarak yeniden dener. Aylık harcama tavanına ulaşıldığında dönen 429 yanıtındaretry-afterbaşlığı yoktur ve tavan kalkana kadar her deneme başarısız olur; kod bu yanıtı yeniden denemez. - Uzun bekleme kiracıya devredilir: Sağlayıcı 30 saniye beklenmesini isterse gateway bağlantıyı açık tutarak beklemez; kiracıya
503ve aynıretry-afterdeğeriyle döner. - Hata ayrıntısı sızdırılmaz:
413ve istek doğrulamasından kaynaklanan400kiracıya aynen iletilir. Anthropic, sizin tanımladığınız harcama sınırına ulaşıldığında da400döndürür; mesajıYou have reached your specifiedile başlayan bu yanıt isteğin değil hesabınızın durumudur. Bu yanıt da kimlik, bakiye ve kapasite hataları gibi gizlenir; kiracı yalnızupstream_unavailablegörür.
Yarım kalan akışlar
Bir akış dört şekilde yarım kalabilir ve her biri farklı ele alınır.
İstemci bağlantıyı kapatır: Kullanıcı sekmeyi kapatır ya da “durdur” düğmesine basar. Örnekteki gateway upstream isteğini iptal eder; böylece kimsenin okumayacağı çıktı için üretim sürmez. Bu kararın bedeli, kesin kullanım bilgisinin gelmemesidir. 10 Ekim 2026 itibarıyla Anthropic ve OpenAI’nin API belgelerinde, senkron bir akışta bağlantı kapandığında üretimin tam olarak ne zaman durduğuna ve o ana kadar üretilen token’ların nasıl faturalandığına dair açık bir tanım bulunmuyor. Bu yüzden kopan istek “bedelsiz” değil “kesinleşmemiş” sayılır; message_start içindeki girdi sayısı ve o ana kadarki çıktı alt sınır olarak kaydedilir. Alternatif, istemci kopsa bile upstream akışını sonuna kadar okumaktır: kesin kullanım bilgisi gelir ama tüm çıktı faturalanır. Yanıtın kendisi saklanacaksa (örneğin arka planda çalışan bir iş) bu tercih anlamlıdır. Her iki durumda da max_tokens tavanı, iptalin geç işlediği senaryoda maliyeti sınırlar. OpenAI’nin Responses API’sindeki arka plan modu ise yanıtı kimliğiyle iptal etmeye izin veren açık bir uç nokta sunar.
Akışın ortasında error olayı gelir: Durum kodu zaten 200 olarak gönderildiği için değiştirilemez. Gateway olayı istemciye olduğu gibi iletir, kaydı kesinleşmemiş olarak işaretler ve isteği kendi başına tekrarlamaz. Anthropic’in belgesi yarıda kalan bir yanıtın nasıl sürdürüleceğini anlatır: alınan kısım saklanır ve devam ettirme isteği gönderilir; araç kullanımı ve düşünme blokları kısmen kurtarılamaz. Bu karar ürün mantığına aittir ve istemci tarafında verilmelidir.
Upstream susar: Belirlenen süre boyunca hiç bayt gelmez. Boşta kalma zamanlayıcısı upstream isteğini iptal eder ve kayıt upstream_idle nedeniyle kesinleşmemiş olarak işaretlenir.
message_stop gelir ama kullanım bilgisi eksiktir: Maliyet hesaplanamaz, kayıt usage_incomplete nedeniyle mutabakata kalır. Bu durum nadirdir ama 0 yazmanın en kolay göründüğü yerdir.
Mutabakat
Gateway’in kendi kayıtları ne kadar dikkatli tutulursa tutulsun, faturanın tek doğru kaynağı sağlayıcıdır. Anthropic’in Usage & Cost Admin API’si kuruluş kullanımını 1 dakikalık, 1 saatlik ya da 1 günlük aralıklarla; API anahtarı, çalışma alanı (workspace) ve modele göre gruplanmış olarak; önbelleksiz girdi, önbellekten okunan girdi, önbelleğe yazılan girdi ve çıktı ayrımıyla döndürür. Maliyet raporu günlük ve USD cinsindendir. Belgeye göre veriler genellikle istek tamamlandıktan sonraki 5 dakika içinde görünür. Bu API kuruluş düzeyinde yönetici yetkisi (Admin API anahtarı ya da org:admin yetkili bir token) gerektirir, çalışma alanı anahtarlarıyla çalışmaz ve bireysel hesaplarda kullanılamaz.
Mutabakatı kolaylaştıran birkaç uygulama:
- Gateway’e ayrı anahtar ya da çalışma alanı verin: Sağlayıcı raporu ile gateway kayıtları aynı kapsamı gösterir ve fark doğrudan karşılaştırılabilir.
- Günlük karşılaştırma yapın: Model ve gün bazında gateway’in kesinleşmiş toplamlarını ve kesinleşmemiş kayıtların alt sınırlarını sağlayıcının token sınıflarıyla karşılaştırın. Fark kesinleşmemiş kayıtlara dağıtılır, eşik aşılırsa uyarı üretilir.
- Sağlayıcının istek kimliğini saklayın: Anthropic her yanıtta bir
request-idbaşlığı döndürür ve destek taleplerinde bu kimlik istenir. - Kayıt yazımını kaybetmeyin: Yanıt gönderildikten sonra veritabanına yazılamayan bir kayıt, gateway’in bildiği tek kullanım kanıtıdır. Bu kayıtlar log satırı olarak bırakılmaz; kalıcı bir kuyruğa ya da outbox tablosuna yazılıp tekrar işlenir. RabbitMQ yazısındaki yayıncı onayı ve dead-letter desenleri bu iş için uygundur.
Güvenlik ve gizlilik
- Sağlayıcı anahtarı: Yalnız gateway sürecinde bulunur, uygulama koduna ve istemciye hiçbir zaman ulaşmaz. Ortam değişkeni yerine bir sır yöneticisi ya da systemd’nin
LoadCredential=mekanizması kullanılabilir. - Kiracı anahtarları: Veritabanında düz metin değil, özet (hash) olarak saklanır ve tek tek iptal edilebilir.
- Girdi sınırları: Gövde boyutu (Messages API için 32 MB), model izin listesi,
max_tokenstavanı veanthropic-betaizin listesi gateway’de uygulanır. - Loglar: İstem ve yanıt metni kişisel veri içerebilir. Varsayılan olarak yalnız istek kimliği, kiracı, model, kullanım ve süre gibi üst veriler loglanır. İçerik saklanacaksa bu bilinçli bir karar olmalı; saklama süresi, maskeleme ve erişim yetkisi KVKK ve sözleşmelerle birlikte belirlenmelidir.
Gözlemlenebilirlik
OpenTelemetry’nin üretken yapay zekâ semantik kuralları (Development aşamasında) kullanım için gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens ve gen_ai.usage.cache_write.input_tokens niteliklerini, yanıtı üreten model için gen_ai.response.model niteliğini tanımlar. Anthropic için toplam girdinin üç alanın toplamı olarak hesaplanması gerektiğini yukarıda gördük; kurallar henüz kararlı olmadığından nitelik adlarını sürüm değişikliklerinde kontrol edin.
İzlenmesi faydalı ölçümler:
- İlk token süresi ve toplam süre: Kullanıcı deneyimini doğrudan gösterir.
- Hata türleri:
overloaded_error,rate_limit_error, boşta kalma iptali ve istemci kopması ayrı ayrı sayılır. - Kesinleşmemiş kayıt oranı: Ani artış, ayrıştırıcı ya da ağ katmanında bir sorunun ilk işaretidir.
- Önbellek oranı: Önbellekten okunan girdinin toplam girdiye oranı; istem yapısındaki bir değişikliğin önbelleği kırıp kırmadığını gösterir.
Test stratejisi
Ayrıştırıcıyı test etmenin en etkili yolu, aynı akışı mümkün olan her noktadan bölüp her seferinde aynı sonucu aldığını doğrulamaktır. Bu test, çok baytlı Türkçe karakterlerin ve \r\n çiftinin parça sınırına denk geldiği durumları kendiliğinden kapsar.
import assert from "node:assert/strict";
import { SseParser } from "./sse-parser.js";
const raw = new TextEncoder().encode(
'event: content_block_delta\r\ndata: {"text":"İstanbul\'da ılık çay"}\r\n\r\n'
);
const whole = new SseParser();
const expected = [...whole.push(raw), ...whole.end()];
assert.equal(expected.length, 1);
for (let cut = 1; cut < raw.length; cut++) {
const parser = new SseParser();
const events = [
...parser.push(raw.subarray(0, cut)),
...parser.push(raw.subarray(cut)),
...parser.end(),
];
assert.deepEqual(events, expected, `bölme noktası: ${cut}`);
}
Bu yazıdaki kod, bayt bölme testinin yanında sahte bir upstream sunucusuyla şu uçtan uca senaryolarda denendi: istemciye giden baytların upstream ile birebir aynı olması, 529 sonrası yeniden deneme, akış ortasında error olayı, istemci kopunca upstream’in iptal edilmesi, upstream’in susması, uzun retry-after, eksik kullanım bilgisi, harcama sınırı 400’ünün gizlenmesi, token sayımının ve kayıt yazımının başarısız olması. Gerçek sağlayıcıdan bir kez kaydedilen akışları (anahtarlar ve içerik temizlenerek) test verisi olarak saklamak, sağlayıcı yeni bir olay türü eklediğinde ayrıştırıcının nasıl davrandığını da görünür kılar.
Üretime almadan önce kontrol listesi
- Akış baytları değiştirilmeden aktarılıyor; tanınmayan olaylar ve
pingistemciye ulaşıyor. - Ayrıştırıcı her bayt bölme noktasında aynı sonucu veriyor;
\r\nve çok baytlı karakterler buna dahil. message_deltakullanım alanları toplanmıyor, üzerine yazılıyor.- Sağlayıcının
usagenesnesi değiştirilmeden saklanıyor; normalleştirilmiş sütunlar sağlayıcıya özgü kurallarla üretiliyor. - Maliyet sürümlü fiyat tablosuyla, tam sayı birimle ve token sınıflarına ayrılarak hesaplanıyor.
- Bütçe istekten önce tek bir SQL ifadesiyle bloke ediliyor; akış süresince transaction açık kalmıyor.
- Yarım kalan istekler 0 değil, alt sınırıyla birlikte kesinleşmemiş olarak kaydediliyor.
- Yeniden deneme yalnız ilk bayttan önce yapılıyor; uzun
retry-afterkiracıya devrediliyor. - Hesap durumunu anlatan hatalar ve
anthropic-ratelimit-*başlıkları kiracıya sızmıyor. - Gateway kayıtları sağlayıcının Usage & Cost raporuyla düzenli olarak karşılaştırılıyor.
Sonuç
LLM gateway’in değeri, sağlayıcıyı uygulamalardan saklamasından çok, her isteğin maliyetini kanıtlanabilir şekilde kayda geçirmesinden gelir. Bunun için akış değiştirilmeden aktarılmalı, kullanım bilgisi sağlayıcının kendi kurallarıyla okunmalı, maliyet sürümlü fiyatlarla tam sayı olarak hesaplanmalı ve bütçe istekten önce bloke edilmelidir. Yarım kalan akışlar kaçınılmazdır; doğru gateway bunları 0 olarak değil, mutabakatla kapanacak açık kayıtlar olarak tutar.
Birincil kaynaklar
- Anthropic: Streaming Messages
- Anthropic: Prompt caching
- Anthropic: Pricing
- Anthropic: API errors
- Anthropic: Rate limits
- Anthropic: Token counting
- Anthropic: Usage and Cost API
- OpenAI: Streaming API responses
- OpenAI: Prompt caching
- OpenAI: Background mode
- OpenAI API Reference: Create chat completion (stream_options)
- WHATWG HTML Standard: Server-sent events
- RFC 9110, Section 7.6.1: Connection
- OpenTelemetry: GenAI semantic conventions (Anthropic dahil)
- MDN: TextDecoder.decode()