
Fri Aug 21 2026

Merhaba! ☕
Bir kahve molası arası sohbet gibi düşünebilirsin bu yazıyı. Hazırsan, birkaç ay önce yaşadığım hikayeyi anlatayım — belki senden de tanıdık gelir.
Sahne: Üretim ortamında çalışan bir LLM tabanlı uygulama geliştirdim. Her şey güzel giderken, birdenbire "gözlemleme", "maliyet takibi", "retry mantığı" ve "fallback" ihtiyacı doğdu.
İlk refleksim: "Hadi bir proxy koyalım arada!" Portkey, Helicone, hatta kendim yazacağım bir middleware... Hepsini değerlendirdim. Dokümanları okudum, demo'ları çalıştırdım, hatta bir kaçı production'a bile aldım.
Sonuç: Hepsini geri aldım. 🔄 Doğrudan OpenAI SDK'ya döndüm.
Şu an aklında "Neden ki? Proxy'ler doch çok özellik sunuyor!" diye soru işaretleri olabilir. Haklısın, sunuyorlar. Ama şu gerçeği de kabul etmek lazım:
| Proxy Katmanı Sunduğu | Bana Gerçekten Lazım Olan |
|---|---|
| Dashboard'lar, loglar, analytics | Kodumun tam kontrolünde olmak |
| Otomatik retry, fallback | Retry mantığını ben yazayım, ben karar verayım |
| Çoklu provider desteği | Tek provider'ı (OpenAI) derinlemesine bilmek |
| Merkezi konfigürasyon | Basitlik — az kod, az hata noktası |
Basitlik ve kontrol, proxy katmanının sunduğu özelliklerden daha değerli olabilir. 🎯
Bu benim tezim. Ve bu yazıda neden böyle düşündüğümü, hangi durumlarda proxy yine de mantıklı olabileceğini, ve nasıl "proxy'siz" ama yine de production-ready bir yapı kurabileceğimizi anlatacağım.
Belki şu an:
Nedenin ne olursa olsun, doğru yerdesin. Hadi birlikte bu konuyu baştan sona inceleyelim — kod örnekleri, trade-off'lar ve pratik tavsiyelerle.
Hazırsan başlayalım! 🚀
Proxy pattern (Portkey, Helicone, kendi middleware'imiz) kullanım kolaylığı ve merkezi gözlem vaat ediyor. Ama üretimde karşılaştığımız gerçekler biraz farklı 🎯
| Özellik | Proxy ile | SDK ile (örnek) |
|---|---|---|
| Loglama | Merkezi proxy logları | logger.info(request_id, payload) |
| Retry / Backoff | Proxy config | @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=0.5)) |
| Fallback | Proxy routing | try: response = call_service() except: response = fallback() |
| Metrics | Proxy exporter | prometheus_client.Counter(...).inc() |
import logging
from tenacity import retry, stop_after_attempt, wait_exponential
logger = logging.getLogger("my_service")
@retry(
wait=wait_exponential(multiplier=0.5, min=1, max=10),
stop=stop_after_attempt(3),
reraise=True,
)
def call_upstream(payload: dict) -> dict:
logger.info("Upstream çağrısı başlıyor", extra={"payload": payload})
resp = http_client.post("/api/upstream", json=payload, timeout=5)
resp.raise_for_status()
logger.info("Upstream yanıtı alındı", extra={"status": resp.status_code})
return resp.json()
def safe_call(payload: dict) -> dict:
try:
return call_upstream(payload)
except Exception as exc:
logger.warning("Upstream başarısız, fallback kullanılıyor", exc_info=exc)
return {"fallback": True, "data": default_response()}
Bu sayede ne oluyor?
Hadi bir sonraki bölümde SDK tabanlı yaklaşımları nasıl ölçeklendirebileceğimize bakalım 🚀
Hadi pratik bir örnek üzerinden anlayalım. API'ye giden diğer tüm o ağ paketçiklerinden, fetch ara katmanından ve "ne zaman yeniden denemeli?" sorusunu yanıtlayan gizli bir servisten kurtulmayı hayal edin. Bunun yerine OpenAI SDK'yı doğrudan kullanın ve kontrol tamamen sizde olsun.
*3 deneme, üstel geri besleme ve tekrarlamalar arasında bekleme süreleri ile donatılmıştır. ✅stream yöntemini kullanın; SDK işleri sizin için parçalara ayırır. ✅usage.prompt_tokens ve usage.completion_tokens her istekle birlikte gelir. Ekstra bir sayaç tutmanız gerekmez. ✅CreateChatCompletionRequest ve ChatCompletion gibi tam nesneler sunar. IDE, bilmediğiniz alanları ve yanlış türde verileri gösterir. ⚠️import { OpenAI } from 'openai'; // tek bağımlılık
import { setTimeout } from 'node:timers/promises';
// İsteğe dair küçük bir wrapper (kendi loglama/ölçüm mizanpajınız için)
async function askGPT(prompt: string): Promise<string> {
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Opsiyonel: ön satış denemesi için küçük bir delay
await setTimeout(50);
const completion = await client.chat.completions.create({
model: 'gpt-4o', // 🎯 model seçimi tamamen benim
messages: [{ role: 'user', content: prompt }],
max_tokens: 150, // 🎯 token kontrolü benzer
temperature: 0.7,
stream: false, // 🎯 streaming desteği, ihtiyaç duyduğumda devre dışı bırakırım
});
// SDK token sayımlarını zaten sağlıyor
console.log(`Usage → ${completion.usage?.total_tokens} token`);
return completion.choices[0]?.message?.content ?? '';
}
Bu sayede ne oluyor?
Görüyorsunuz, sadece birkaç satır ve birkaç tip tanımı ile tam bir istemciye sahibiz. SDK şunları yapar:
model, messages vb.)*3 denemeden sonra hata fırlatırcompletion.usage)model: 123) göstermeden önce hata verirgpt-4o, claude-3-opus, herhangi bir SDK tarafından desteklenen bir model.temperature, max_tokens, top_p, presence_penalty vb. SDK, bunları açıkça mümkün olanları listeleyerek kontrol eder.retry fonksiyonu yazabilirsiniz; varsayılan strateji zaten iyi bir başlangıç noktasıdır.npm i openai yapın ve hemen başlayın.jest veya vitest ile kontrolü kolaylaştırabilirsiniz.node:alpine + openai paketinden oluşuyor; ekstra bir istek proxy sunucusu, JSON dönüştürücüsü veya istemci kitaplığı olmayacak.İstediğiniz retry sayısını, model seçimini ve hatta istek öncesi loglama mekanizmasını tamamen kendi kodunuzda tanımlayabilirsiniz. Bu, kontrolünüzün gerçekten sizde olduğu bir yaklaşım.
Hazırsan doğrudan koda atalım. OpenAI SDK'yı doğrudan kullanarak, production-ready bir wrapper nasıl yazılır görüyoruz. Kodun içinde: client ayarları, loglama, token/maliyet hesabı, retry/fallback ve streaming — hepsi tek dosyada 🎯
openai-direct-wrapper.tsimport OpenAI from 'openai';
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
// ──────────────────────────────────────────────
// 1️⃣ Client init — timeout & maxRetries merkezi yerden
// ──────────────────────────────────────────────
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
timeout: 30_000, // 30 sn — ağ takılmasın
maxRetries: 2, // SDK kendi retry'lerini yapsın
});
// Basit maliyet tablosu (USD / 1K token) — güncellemeyi unutma ⛔
const PRICING: Record<string, { input: number; output: number }> = {
'gpt-4o': { input: 0.005, output: 0.015 },
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
};
// ──────────────────────────────────────────────
// 2️⃣ Yardımcı: maliyet hesapla (FinOps için)
// ──────────────────────────────────────────────
function estimateCost(model: string, promptTokens: number, completionTokens: number): number {
const rates = PRICING[model] ?? PRICING['gpt-4o-mini']; // bilinmeyen modelde en ucuzu varsay
return (promptTokens / 1_000) * rates.input + (completionTokens / 1_000) * rates.output;
}
// ──────────────────────────────────────────────
// 3️⃣ Ana wrapper — tek fonksiyon, her şey dahil
// ──────────────────────────────────────────────
interface ChatOptions {
model?: string;
messages: ChatCompletionMessageParam[];
temperature?: number;
maxTokens?: number;
stream?: boolean;
fallbackModel?: string; // retry sonrası denenecek model
}
interface ChatResult {
content: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
estimatedCostUsd: number;
};
modelUsed: string;
}
async function chatCompletion({
model = 'gpt-4o-mini',
messages,
temperature = 0.2,
maxTokens,
stream = false,
fallbackModel = 'gpt-3.5-turbo',
}: ChatOptions): Promise<ChatResult> {
const start = Date.now();
let lastError: Error | null = null;
// 🔁 İlk deneme + fallback = max 2 model
for (const currentModel of [model, fallbackModel]) {
try {
console.log(`🚀 [${currentModel}] İstek gönderiliyor…`, { messageCount: messages.length });
const response = await openai.chat.completions.create({
model: currentModel,
messages,
temperature,
max_tokens: maxTokens,
stream,
});
// ── Streaming DEĞILSE ──
if (!stream) {
const choice = response.choices[0];
const content = choice.message.content ?? '';
const usage = response.usage!;
const cost = estimateCost(currentModel, usage.prompt_tokens, usage.completion_tokens);
console.log(`✅ [${currentModel}] Başarılı`, {
latencyMs: Date.now() - start,
tokens: usage.total_tokens,
costUsd: cost.toFixed(6),
});
return {
content,
usage: {
promptTokens: usage.prompt_tokens,
completionTokens: usage.completion_tokens,
totalTokens: usage.total_tokens,
estimatedCostUsd: cost,
},
modelUsed: currentModel,
};
}
// ── Streaming İSE — chunk'ları toplayıp tek string döndürüyoruz ──
let fullContent = '';
let promptTokens = 0;
let completionTokens = 0;
for await (const chunk of response) {
const delta = chunk.choices[0]?.delta?.content ?? '';
fullContent += delta;
// usage sadece son chunk'ta gelir (OpenAI API spesifikasyonu)
if (chunk.usage) {
promptTokens = chunk.usage.prompt_tokens;
completionTokens = chunk.usage.completion_tokens;
}
}
const cost = estimateCost(currentModel, promptTokens, completionTokens);
console.log(`✅ [${currentModel}] Streaming tamam`, {
latencyMs: Date.now() - start,
tokens: promptTokens + completionTokens,
costUsd: cost.toFixed(6),
});
return {
content: fullContent,
usage: {
promptTokens,
completionTokens,
totalTokens: promptTokens + completionTokens,
estimatedCostUsd: cost,
},
modelUsed: currentModel,
};
} catch (err) {
lastError = err as Error;
console.warn(`⚠️ [${currentModel}] Hata, fallback deneniyor…`, { error: err });
// sonraki modele geç
}
}
// Hiçbiri olmadıysa hata fırlat
throw new Error(`Tüm modeller başarısız oldu. Son hata: ${lastError?.message}`);
}
// ──────────────────────────────────────────────
// 4️⃣ Kullanım örnekleri — kopyala, çalıştır 🎉
// ──────────────────────────────────────────────
// --- Normal (non-streaming) çağrı ---
async function normalExample() {
const result = await chatCompletion({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: 'Sen yardımcı bir asistanın.' },
{ role: 'user', content: 'TypeScript\'te interface vs type farkı nedir? Kısaca anlat.' },
],
temperature: 0.3,
maxTokens: 300,
});
console.log('\n📝 Cevap:', result.content);
console.log('📊 Kullanım:', result.usage);
console.log('🤖 Model:', result.modelUsed);
}
// --- Streaming çağrı ---
async function streamingExample() {
const result = await chatCompletion({
model: 'gpt-4o',
messages: [
{ role: 'user', content: '1-10 arası sayıları tek tek yaz, arada 100ms bekle.' },
],
stream: true,
maxTokens: 200,
});
console.log('\n📝 Cevap (streaming sonrası birleştirilmiş):', result.content);
console.log('📊 Kullanım:', result.usage);
}
// Çalıştır
// await normalExample();
// await streamingExample();
export { chatCompletion, estimateCost, openai };
| Parça | Ne İşe Yarar? |
|---|---|
| Client init | timeout + maxRetries merkezi — her çağrıda tekrar yazmazsın 🛠 |
chatCompletion wrapper |
Tek fonksiyon: loglama ✅, token sayımı ✅, maliyet tahmini ✅, retry/fallback ✅ |
| Fallback mantığı | İlk model hata verirse fallbackModel ile ikinci şans — kullanıcı beklemez 🔁 |
| Streaming desteği | stream: true verince chunk'ları toplar, aynı döndürü tipini korur 🌊 |
| Maliyet hesabı | estimateCost ile anlık FinOps verisi — fatura sürprizi yok 💰 |
Evet. SDK'nın verdiği timeout, maxRetries, stream seçeneklerini doğru birleştirip, tek bir wrapper içine sardık. Artık projenin her yerinden chatCompletion({…}) diyorsun — log, maliyet, fallback hepsi otomatik geliyor.
İpucu:
PRICINGobjesini bir config dosyasından veya environment variable'dan okuyup, deploy etmeden güncelleyebilirsin. Hardcode etmemenin yolu bu 😉
Hadi şimdi test et — npx ts-node openai-direct-wrapper.ts (veya tsx ile) çalıştır, konsolda logları izle. Sorun mu var? Yanındayım 🤝
Proxy araçları güzel dashboard'lar sunuyor, haklısınız. Ama fiyat değiştiğinde ne oluyor? Dashboard güncellenene kadar bekliyorsunuz. Yeni bir model çıktığında? Yine beklemektesiniz. Ben kendi verimi, kendi mantığımla tutmayı tercih ediyorum — şeffaf, esnek ve sıfır bağımlılık 🎯
Her LLM isteği sonrası şu üç şeyi bir tabloya atıyoruz:
token_usage (prompt + completion)model (hangi model?)estimated_cost (o anki fiyatlandırma ile hesaplı maliyet)Sonra ister Grafana, ister Prometheus, ister basit bir SQL sorgusu ile aylık özet çekiyoruz. Proxy dashboard'ının "güncellemeyi unutması" riski yok 🛠
# config/pricing.py — tek yerde, versiyon kontrolünde
MODEL_PRICING = {
"gpt-4o": {"input": 5.00, "output": 15.00}, # $ per 1M tokens
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
# yeni model eklendi mi? Buraya bir satır ekle, deploy et, bitti ✅
}
def calculate_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
pricing = MODEL_PRICING.get(model)
if not pricing:
return 0.0 # bilinmeyen model → logla, alarm ver, ama sistemi yıkma
input_cost = (prompt_tokens / 1_000_000) * pricing["input"]
output_cost = (completion_tokens / 1_000_000) * pricing["output"]
return round(input_cost + output_cost, 6)
# service/llm_logger.py
def log_llm_usage(request_id: str, model: str, prompt_tokens: int, completion_tokens: int):
cost = calculate_cost(model, prompt_tokens, completion_tokens)
db.execute("""
INSERT INTO llm_usage (request_id, model, prompt_tokens, completion_tokens, estimated_cost, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
""", (request_id, model, prompt_tokens, completion_tokens, cost))
Ne oldu? Fiyat değiştiğinde MODEL_PRICING dict'ini güncelliyorsun, tek dosya, tek commit, anında etkili. Proxy ekibinin sprint planlamasını beklemiyorsun ❗️
SELECT
model,
COUNT(*) AS request_count,
SUM(prompt_tokens + completion_tokens) AS total_tokens,
ROUND(SUM(estimated_cost), 4) AS total_cost_usd
FROM llm_usage
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
GROUP BY model
ORDER BY total_cost_usd DESC;
Çıktı örneği:
| model | request_count | total_tokens | total_cost_usd |
|---|---|---|---|
| gpt-4o | 12,450 | 38,200,000 | 421.50 |
| gpt-4o-mini | 89,300 | 15,600,000 | 12.80 |
Bu sorguyu Metabase / Redash / Supabase / psql nerede çalıştırırsan çalıştır, aynı sonucu verir. Proxy'ye özel API, token, rate limit... yok 🔁
created_at + model → yeterlipricing.py tek kaynak (single source of truth)time series panel, Slack'e haftalık bot, CSV export — hepsi aynı veridencalculate_cost() pure function → unit test yaz, CI'da koştur, güvenle deploy et ✅estimated_cost kolonunu float değil, numeric(18,6) veya integer (mikro-dolar / 1_000_000) olarak tut. Floating-point hassasiyet sorunlarıyla ay sonunda "$0.01 fark" demek istemezsin ⛔
Özetle: Proxy maliyet dashboard'ı "güzel bir ekstra" ama kendi verini tutmak "kendine ait bir hak". 50 satır Python + 1 SQL sorgusuyla takımına her ay başında "Bu ay gpt-4o $421, gpt-4o-mini $13" diyorsun. Proxy beklemek değil, verini ele geçirmek bu 🎯
Proxy araçları (Portkey, Helicone) kullanışlı ama kendi formatlarına bağlarsın 🎯.
Doğrudan SDK ile çalışırken sadece OpenAI SDK’na bağlı kalırsın.
Adapter pattern ile Anthropic, Gemini, yerel modeller (Ollama) için tek bir arayüz sağlarsın.
Böylece yarın model değiştirmek istersen sadece yeni bir class eklersin, proxy aracının güncellenmesini beklemek zorunda kalmazsın ✅.
if (provider === 'openai') … dağıtılırLLMProvider arayüzü 🛠Tek bir contract tanımlarız, her sağlayıcı bu contract’ı uygular.
Factory ile istediğimiz implementasyonu runtime’da seçeriz.
// 1️⃣ Ortak arayüz
export interface LLMProvider {
/** Tek bir mesaj gönderir ve cevabı döner */
chat(messages: ChatMessage[]): Promise<string>;
}
/** Mesaj yapısı – her sağlayıcı için ortak */
export type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string };
// 2️⃣ OpenAI implementasyonu
export class OpenAIProvider implements LLMProvider {
constructor(private readonly apiKey: string) {}
async chat(messages: ChatMessage[]): Promise<string> {
// OpenAI SDK çağrısı (basitleştirilmiş)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o', messages }),
});
const data = await response.json();
return data.choices[0]?.message?.content ?? '';
}
}
// 3️⃣ Anthropic implementasyonu
export class AnthropicProvider implements LLMProvider {
constructor(private readonly apiKey: string) {}
async chat(messages: ChatMessage[]): Promise<string> {
// Anthropic SDK çağrısı (basitleştirilmiş)
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-3-opus-20240229',
messages: messages.map(m => ({ role: m.role, content: m.content })),
}),
});
const data = await response.json();
return data.content[0]?.text ?? '';
}
}
// 4️⃣ Basit factory – runtime’da seçim
export function createProvider(type: 'openai' | 'anthropic', apiKey: string): LLMProvider {
switch (type) {
case 'openai':
return new OpenAIProvider(apiKey);
case 'anthropic':
return new AnthropicProvider(apiKey);
default:
throw new Error(`Bilinmeyen provider: ${type}`);
}
}
const provider = createProvider('openai', process.env.OPENAI_KEY!);
const answer = await provider.chat([
{ role: 'system', content: 'Sen yardımcı bir asistanısın.' },
{ role: 'user', content: 'Merhaba!' },
]);
console.log(answer);
Ne kazandı bu mimari? 🎉
LLMProvider yazarak unit testleriniz hızlı ve izole olur.Kısaca: Kendi adapter’ını yaz, proxy’a bağımlı kalma 🛡. Gelecekte hangi model gelirse gelsin, kodun hareket etmez.
Proxy pattern yanlış değil — sadece her proje için varsayılan doğru değil 🎯
Küçük/orta ölçekli takımlar, hızlı iterasyon isteyenler, maliyet ve hata noktalarını minimumda tutmak isteyenler için:
Doğrudan SDK + kendi ince wrapper genelde en iyi ROI'yi verir 🛠
Proxy katmanını kaldırdıktan sonra pipeline temizlendi, debug süresi kısaldı, fatura şaşırtmadı.
Basitlik bir özelliktir, proxy ise bir tercihtir. Doğrusunu sen bilirsin 😉
Hazırsan kısaca özetleyelim — küçük takımlarda hız ve güvenilirlik dengesi hayat kurtarıyor 🚀
maxRetries ve timeout ayarlarını mutlaka yap
OpenAI SDK’sında varsayılanlar bazen yeterli değil. Ağ dalgalanmasında sonsuz bekleme veya gereksiz retry’ler maliyeti şişirir.
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
max_retries=3, # 🎯 en fazla 3 deneme
timeout=15.0 # ⏱ 15 sn sonra kes
)
Bu sayede ağ hatalarında kontrollü bir geri dönüş alırsın.
Token kullanımı ve maliyet için tek bir yardımcı fonksiyon yaz, her çağrıda logla
def log_usage(response, model: str):
usage = response.usage
cost = (usage.prompt_tokens * INPUT_PRICE[model] +
usage.completion_tokens * OUTPUT_PRICE[model]) / 1_000_000
logger.info(
"model=%s prompt=%d completion=%d cost=$%.6f",
model, usage.prompt_tokens, usage.completion_tokens, cost
)
Her istekte bu fonksiyonu çağırırsan maliyet takibi tek noktadan olur.
Feature flag ile model değiştirme (gpt‑4o → gpt‑4o‑mini) canlıda test et
Proxy gerçekten gerekirse (enterprise audit, çoklu provider routing) o zaman ekle, erken optimizasyondan kaçın
Soruların olursa yorumlarda buluşalım 🤝
Mutlu kodlamalar! ✨
Bu içerik tamamen yapay zeka destekli otomasyon sistemi ile üretilmiştir.
All rights reserved