Yapay zeka uygulamalarında satıcıya kilitlenme genellikle bir anda olmaz. Sinsice sızar — burada doğrudan import openai, şurada hardcode edilmiş bir model adı, başka sağlayıcıların aynı şeyi döndürüp döndürmediğini kontrol etmeden ayrıştırdığınız bir yanıt alanı. Altı ay sonra, sağlayıcı değiştirmek arka ucunuzun yarısını yeniden yazmak anlamına gelir.
Kilitlenmenin gerçekleştiği dört yol
Çoğu geliştirici, kilitlenmenin “OpenAI SDK’sını kullanıyorum” anlamına geldiğini düşünür. Bu en az tehlikeli türdür. Asıl tuzaklar daha sinsi:
| Kilitlenme türü | Nasıl olur | Sonuç |
|---|---|---|
| SDK kilitlenmesi | from openai import OpenAI her yerde | SDK değiştirmek her dosyaya dokunmayı gerektirir |
| Model adı kilitlenmesi | model="gpt-4o" iş mantığına hardcode edilir | Her model değişikliği bir kod değişikliğidir |
| Parametre kilitlenmesi | logprobs, n>1 veya reasoning_effort kullanmak | Bunlar Claude veya Gemini’de yoktur |
| Yanıt biçimi kilitlenmesi | Sağlayıcıya özgü yanıt alanlarını ayrıştırmak | Farklı sağlayıcılar farklı biçimler döndürür |
Amaç bunların hepsini ortadan kaldırmak değil — bazıları kabul edilebilir ödünlerdir. Amaç, hangilerini üstlendiğinizi bilmek.
Soyutlama katmanı olarak OpenAI uyumlu bir uç nokta kullanın
SDK kilitlenmesinden kaçınmanın en temiz yolu, birden çok sağlayıcıya yönlendiren tek bir OpenAI uyumlu uç nokta kullanmaktır. OpenAI SDK’sını korursunuz, ancak arka uç herhangi bir sağlayıcı olabilir.
CometAPI bunu yapar — tek uç nokta, tek anahtar, OpenAI, Anthropic, Google, DeepSeek, xAI ve diğerleri genelinde 500+ model:
import osfrom openai import OpenAIfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("AI_API_KEY")if not api_key: raise ValueError("AI_API_KEY environment variable is not set")client = OpenAI( base_url=os.environ.get("AI_BASE_URL", "https://api.cometapi.com/v1"), api_key=api_key,)
GPT’den Claude’a, oradan Gemini’ye geçiş tek satırlık bir değişikliktir:
# Beforeresponse = client.chat.completions.create(model="gpt-5.4", messages=[...])# After — same code, different modelresponse = client.chat.completions.create(model="claude-sonnet-4-6", messages=[...])
Not: gpt-5.4 ve claude-sonnet-4-6 gibi model adları CometAPI’nin platform tanımlayıcılarıdır — yalnızca https://api.cometapi.com/v1 üzerinden çalışır, OpenAI veya Anthropic’in API’leri üzerinden doğrudan çalışmaz. Tam katalog ve fiyatlandırma için tam model listesi bölümüne bakın.
Model adlarını iş mantığınızın dışına çıkarın
Koda dağılmış model adları, en yaygın kilitlenme türüdür. Çözüm, ortam değişkenlerinden okuyan merkezi bir yapılandırmadır:
# config.py — one place to change model assignmentsimport osMODEL_CONFIG = { "summarize": os.environ.get("MODEL_SUMMARIZE", "claude-opus-4-7"), "code": os.environ.get("MODEL_CODE", "gpt-5.4"), "classify": os.environ.get("MODEL_CLASSIFY", "claude-haiku-4-5"), "chat": os.environ.get("MODEL_CHAT", "gpt-5.4-mini"),}# Validate at startup — fail fast rather than getting mysterious API errorsfor task, model in MODEL_CONFIG.items(): if not model: raise ValueError(f"Model config for '{task}' is not set")
İş mantığınız asla doğrudan bir model adına başvurmaz:
from config import MODEL_CONFIGdef summarize(text: str) -> str: response = client.chat.completions.create( model=MODEL_CONFIG["summarize"], messages=[{"role": "user", "content": f"Summarize: {text}"}], max_tokens=300 # move to config in production ) return response.choices[0].message.content
Uygulamanız genelinde özetleme modelini değiştirmek için bir ortam değişkenini değiştirmeniz yeterlidir. Ne grep, ne bul-değiştir.
Kodu sağlayıcıya özgü alanlara bağımlı kılmamak için yanıtı sarmalayın
Farklı sağlayıcılar biraz farklı yanıt biçimleri döndürür. Kod tabanı boyunca ham API yanıtlarını ayrıştırırsanız, o sağlayıcının biçimine kilitlenmiş olursunuz.
Bunu normalize edilmiş bir dataclass içine sarın:
from dataclasses import dataclassfrom typing import Optionalfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom openai.types.chat import ChatCompletionimport logging@dataclassclass AIResponse: content: str model: str input_tokens: int output_tokens: intdef call_model(task: str, messages: list, **kwargs) -> AIResponse: """ Single entry point for all LLM calls. Returns a normalized AIResponse regardless of which model handled it. Raises on 4xx (client errors). Logs and re-raises on 5xx/network errors. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for task '{task}'") try: response: ChatCompletion = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: logging.error(f"API error for task={task} model={model}: {e.status_code} {e.message}") raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network error for task={task} model={model}: {e}") raise # content is None when the model triggers a tool call instead of returning text content = response.choices[0].message.content or "" # usage is None in streaming mode — default to 0 if not available usage = response.usage input_tokens = usage.prompt_tokens if usage else 0 output_tokens = usage.completion_tokens if usage else 0 logging.info( f"task={task} model={model} " f"input_tokens={input_tokens} output_tokens={output_tokens}" ) return AIResponse( content=content, model=response.model, input_tokens=input_tokens, output_tokens=output_tokens, )
Artık iş mantığınız ham API yanıtlarıyla değil, AIResponse nesneleriyle çalışır. Bir sağlayıcı yanıt biçimini değiştirirse, bunu tek bir yerde düzeltirsiniz.
Sarmalayıcıya akış desteği ekleyin
Sohbet arayüzleri için akış isteyeceksiniz. Sarmalayıcı bunu ayrı bir yol olarak ele alır:
from typing import Iteratordef stream_model(task: str, messages: list, **kwargs) -> Iterator[str]: """ Stream tokens from the routed model. Note: streaming doesn't return usage data. Fallback is not supported in streaming mode — you've already started yielding tokens before you know if the full request succeeds. """ model = MODEL_CONFIG.get(task, "gpt-5.4-mini") if not model: raise ValueError(f"No model configured for task '{task}'") stream = client.chat.completions.create( model=model, messages=messages, stream=True, **kwargs ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: yield delta# Usagefor token in stream_model("chat", [{"role": "user", "content": "Hello"}]): print(token, end="", flush=True)
Hangi parametrelerin kilitlenme yarattığını bilin
Bazı parametreler yalnızca belirli sağlayıcılarda vardır. Bunları kullanmak sorun değil — sadece bilinçli bir tercih yaptığınızdan emin olun:
| Parametre | Nerede çalışır | Kilitlenme riski |
|---|---|---|
| logprobs | Yalnızca GPT | Yüksek — Claude veya Gemini’de karşılığı yok |
| n > 1 | GPT, Gemini (Claude değil) | Orta — Claude için döngü gerekir |
| reasoning_effort | Yalnızca GPT o-serisi | Yüksek — başka yerde karşılığı yok |
| temperature > 1.0 | GPT, Gemini (Claude değil) | Düşük — Claude 1.0’da sınırlar |
| tools | Tüm büyük sağlayıcılar | Yok — kullanmak güvenli |
| response_format | Tüm büyük sağlayıcılar | Düşük — küçük şema farklılıkları |
Eğer güven puanlaması için logprobs kullanıyorsanız, bu özellik için GPT’ye kilitlenmişsiniz demektir. Bu makul bir ödündür — yalnızca bunu belgelendirin ki sonraki geliştirici nedenini bilsin.
Sağlayıcı uç noktasını yapılandırılabilir yapın
base_url="https://api.cometapi.com/v1" değerini hardcode etmek hâlâ bir kilitlenme biçimidir. Bunu bir ortam değişkenine bağlayın:
# .env — using CometAPIAI_BASE_URL=https://api.cometapi.com/v1AI_API_KEY=your_cometapi_key# To switch to OpenAI directly, change two lines:# AI_BASE_URL=https://api.openai.com/v1# AI_API_KEY=your_openai_key
- Adımdaki istemci başlatma zaten bu değişkenlerden okuyor. CometAPI ile doğrudan bir sağlayıcı arasında geçiş yapmak artık bir kod değişikliği değil, bir yapılandırma değişikliğidir.
Node.js sürümü
import OpenAI from 'openai';const apiKey = process.env.AI_API_KEY;if (!apiKey) throw new Error('AI_API_KEY is not set');const client = new OpenAI({ baseURL: process.env.AI_BASE_URL ?? 'https://api.cometapi.com/v1', apiKey,});// Model IDs are CometAPI platform identifiers — see cometapi.com/modelsconst MODEL_CONFIG = { summarize: process.env.MODEL_SUMMARIZE ?? 'claude-opus-4-7', code: process.env.MODEL_CODE ?? 'gpt-5.4', classify: process.env.MODEL_CLASSIFY ?? 'claude-haiku-4-5', chat: process.env.MODEL_CHAT ?? 'gpt-5.4-mini',};// Validate at startupfor (const [task, model] of Object.entries(MODEL_CONFIG)) { if (!model) throw new Error(`Model config for '${task}' is not set`);}/** * Single entry point for all LLM calls. * Returns normalized response. Raises on 4xx, logs and re-raises on 5xx/network. */async function callModel(task, messages, options = {}) { const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini'; let response; try { response = await client.chat.completions.create({ model, messages, ...options, }); } catch (err) { // Don't swallow errors — log and re-raise console.error(`API error task=${task} model=${model}:`, err.message); throw err; } // content is null when model triggers a tool call const content = response.choices[0].message.content ?? ''; // usage may be absent in some configurations const inputTokens = response.usage?.prompt_tokens ?? 0; const outputTokens = response.usage?.completion_tokens ?? 0; console.log(`task=${task} model=${model} input=${inputTokens} output=${outputTokens}`); return { content, model: response.model, inputTokens, outputTokens };}/** * Stream tokens from the routed model. * Usage data is not available in streaming mode. */async function* streamModel(task, messages, options = {}) { const model = MODEL_CONFIG[task] ?? 'gpt-5.4-mini'; const stream = await client.chat.completions.create({ model, messages, stream: true, ...options, }); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content; if (delta) yield delta; }}// Usage — blockingconst result = await callModel('classify', [ { role: 'user', content: 'Positive or negative? "Loved it!"' }]);console.log(result.content);// Usage — streamingfor await (const token of streamModel('chat', [ { role: 'user', content: 'Hello' }])) { process.stdout.write(token);}
Hangi kilitlenme kabul edilebilir
Her kilitlenme ile savaşmaya değmez. Bazı ödünler mantıklıdır:
- Kullanmak OpenAI SDK — De facto standarttır. Çoğu sağlayıcı bunu destekler. Düşük riskli kilitlenme.
- Gerçekten ihtiyaç duyduğunuz sağlayıcıya özgü özellikler —
logprobsgerekiyorsa, kullanın. Bu kodu izole edin ki daha sonra bulup değiştirmek kolay olsun. - İnce ayarlı modeller — İnce ayarlı bir model doğası gereği tek bir sağlayıcıya bağlıdır. Bu beklenir.
Kaçınmaya değer kilitlenme, kazara olanıdır — iş mantığında model adları, dosyalara yayılmış ham yanıt ayrıştırma, kaynakta hardcode edilmiş API anahtarları.
Sırada ne var
Artık sağlayıcı ayrıntılarını iş mantığınızın dışında tutan bir soyutlama katmanına sahipsiniz. Bu serinin son yazısı, işler ters gittiğinde ne olduğunu kapsar: başarısız üretimleri nasıl hata ayıklayacağınız, hata kodlarını nasıl yorumlayacağınız ve gerçekten neyin bozulduğunu söyleyen hata yönetimini nasıl kuracağınız.
Sıradaki: Başarısız AI API Üretimlerini Nasıl Hata Ayıklarsınız
SSS
S: SDK kilitlenmesi ile model kilitlenmesi arasındaki fark nedir?
SDK kilitlenmesi, kodunuzun belirli bir kütüphaneyi içe aktardığı ve SDK’ları değiştirdiğinizde değişmesi gerektiği anlamına gelir. Model kilitlenmesi ise model adlarının iş mantığınız boyunca dağılmış olduğu anlamına gelir. SDK kilitlenmesi daha az tehlikelidir çünkü çoğu sağlayıcı artık OpenAI SDK biçimini desteklemektedir. Model kilitlenmesi daha sinsi bir durumdur çünkü bulması ve düzeltmesi daha zordur.
S: CometAPI kullanırsam, sadece OpenAI kilitlenmesini CometAPI kilitlenmesiyle mi değiştiriyorum?
Kısmen. Doğrudan sağlayıcı kilitlenmesini bir proxy katmanıyla değiştiriyorsunuz. Artıları: tek anahtar, tek uç nokta, kolay model değiştirme. Riski: CometAPI bir kesinti yaşarsa, tüm sağlayıcılarınız birlikte gider. Azaltım, yukarıdaki koddadır — AI_BASE_URL bir ortam değişkenidir. CometAPI’yi atlayıp bir sağlayıcıyı doğrudan çağırmanız gerekirse, bu bir yapılandırma değişikliği, kod değişikliği değil.
S: Claude’un genişletilmiş düşünmesini veya OpenAI’nin reasoning_effort özelliğini bu desenle kullanabilir miyim?
Evet, bunları call_model’a **kwargs olarak aktarın. Yalnız, bu görevi farklı bir modele yönlendirirseniz bu parametreler yok sayılabilir veya hata oluşturabilir. Hangi görevlerin sağlayıcıya özgü özellikler kullandığını belgelendirin ki sonraki geliştirici nedenini bilsin.
S: Claude’un temperature değerini 1.0’da sınırlamasını, Claude ile GPT**?** arasında yönlendirirken nasıl ele alırım?
Her iki tarafta da güvenli bölgede kalmak için temperature’ı 1.0 veya altında tutun. Özellikle GPT’de yaratıcı görevler için daha yüksek sıcaklığa ihtiyacınız varsa, bu görevleri MODEL_CONFIG içinde GPT’ye açıkça yönlendirin; genel yönlendiriciye bırakmayın.
S: Görüntü ve video üretim API’lerini de aynı şekilde soyutlamalı mıyım?
Aynı ilkeler geçerlidir — merkezi yapılandırma, normalize edilmiş yanıt sarmalayıcı, iş mantığında sağlayıcıya özgü alanlar yok. Görüntü ve video API’lerinde daha fazla yapısal fark vardır (eşzamansız vs eşzamanlı, farklı parametre setleri), bu yüzden soyutlama katmanı daha fazla çalışma gerektirir. Metinle başlayın, yapı kanıtlandıktan sonra kalıbı genişletin.
S: Modeller arasındaki bağlam penceresi farklılıkları ne olacak?
Yönlendirme yaparken bu gerçek bir risktir. GPT-5.5 1M token bağlam penceresine sahiptir, Claude modelleri 200K’ya kadar destekler ve Gemini 3.5 Flash 1M’e kadar destekler. Uzun bir belge görevini daha kısa bağlam penceresine sahip bir modele yönlendirirseniz, giriş sessizce kırpılır. Görevleriniz uzun girdiler içeriyorsa yönlendirmeden önce bir bağlam uzunluğu kontrolü ekleyin — veya uzun bağlamlı görevleri her zaman MODEL_CONFIG içinde belirli bir modele yönlendirin; varsayılanla düşmesine izin vermeyin.
