/** * Z.ai (GLM) doğrudan istemcisi — uygulamanın BİRİNCİL yapay zekâ sağlayıcısı. * * Neden: 2026-09'da OpenRouter anahtarı iptal olup hesap bakiyesi eksiye * düşünce şartname analizi, TED çevirisi ve site taraması durdu. Z.ai'nin * kendi API'si (Coding Plan aboneliği) ayrı faturalanır; OpenRouter'daki bir * sorun artık uygulamayı durdurmaz. OpenRouter yalnız yedek olarak kalır. * * Ortam değişkenleri: * ZAI_API_KEY zorunlu (yoksa Z.ai devre dışı) * ZAI_BASE_URL varsayılan genel API; Coding Plan için * https://api.z.ai/api/coding/paas/v4/chat/completions * ZAI_REASONING_EFFORT glm-5.x düşünme seviyesi (varsayılan low) * ZAI_MAX_CONCURRENCY eşzamanlı istek sınırı (varsayılan 2) * ZAI_RETRIES_429 oran sınırında aynı modelle yeniden deneme (varsayılan 4) * ZAI_RETRY_BASE_MS yeniden deneme taban beklemesi (varsayılan 3000) * ZAI_TRANSLATE_MODELS çeviri model listesi (virgülle; varsayılan glm-5.3,glm-5.3-flash) */ const VARSAYILAN_URL = 'https://api.z.ai/api/paas/v4/chat/completions'; // TED başlıkları kullanıcıya doğrudan görünür ve paketler küçüktür (6 ilan); // canlıda glm-5.3-flash başlıkların ~%20'sinde harf bozdu (kelime içi İ), // bu yüzden önce GLM 5.3, Flash yedek. export const DEFAULT_ZAI_TRANSLATE_MODELS = ['glm-5.3', 'glm-5.3-flash']; /** Anahtar çağrı anında okunur; süreç yeniden başlatılmadan da ayar değişebilir. */ export const zaiKey = () => (process.env.ZAI_API_KEY || '').trim() || null; export const zaiUrl = () => (process.env.ZAI_BASE_URL || '').trim() || VARSAYILAN_URL; export function zaiTranslateModels() { const liste = (process.env.ZAI_TRANSLATE_MODELS || '').split(',').map((m) => m.trim()).filter(Boolean); return liste.length ? liste : DEFAULT_ZAI_TRANSLATE_MODELS; } /** * Düşünme (reasoning) ayarı modele göre değişir — 2026-09-10 canlı ölçümü: * - glm-4.x: `thinking: {type:"disabled"}` kabul edilir. * - glm-5.x: düşünme KAPATILAMAZ (hata 1210). `reasoning_effort: "low"` ile * 0 düşünme tokenı ve kusursuz kaliteyle, varsayılan ayarın ~5 katı hızda * çalışır. null → sağlayıcı varsayılanı (en yavaş). */ export function dusunmeAyari(model, effort) { if (/^glm-5/i.test(model)) { if (effort === null) return {}; return { reasoning_effort: effort || (process.env.ZAI_REASONING_EFFORT || '').trim() || 'low' }; } return { thinking: { type: 'disabled' } }; } /** * Z.ai 429 yanıtını ikiye ayırır: 1113 (bakiye/abonelik yetersiz) beklemekle * düzelmez; 1302/1305 (oran sınırı / sunucu yoğun) kısa beklemeyle geçebilir. */ export const gecici429 = (govde) => !/"code"\s*:\s*"?1113"?/.test(String(govde || '')); /** * Süreç genelinde eşzamanlı Z.ai isteği sınırı. Coding Plan aynı anda en * fazla 2 isteğe izin veriyor (3. istek 1302 alıyor). TED çevirisi, analiz ve * site taraması aynı anda çalışabildiği için sınır tek noktada uygulanır. * Öncelik: boşalan yer önce kullanıcının beklediği isteklere ("yuksek") * verilir; arka plan çevirisi ("dusuk") en fazla bir istek süresi bekler. */ let aktifIstek = 0; const kuyruk = { yuksek: [], dusuk: [] }; const eszamanliSinir = () => Math.max(1, Number(process.env.ZAI_MAX_CONCURRENCY ?? 2) || 2); async function yerAl(oncelik) { if (aktifIstek < eszamanliSinir()) { aktifIstek++; return; } await new Promise((r) => (kuyruk[oncelik] || kuyruk.dusuk).push(r)); } function yerBirak() { const siradaki = kuyruk.yuksek.shift() ?? kuyruk.dusuk.shift(); if (siradaki) siradaki(); // yer doğrudan sıradakine devredilir else aktifIstek = Math.max(0, aktifIstek - 1); } /** Test ve gözlem için: o an açık istek sayısı. */ export const zaiAktifIstek = () => aktifIstek; export class ZaiError extends Error { constructor(message, status) { super(message); this.name = 'ZaiError'; this.status = status; } } const bekle = (ms, signal) => new Promise((resolve, reject) => { if (signal?.aborted) return reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); const t = setTimeout(() => { signal?.removeEventListener('abort', iptal); resolve(); }, ms); const iptal = () => { clearTimeout(t); reject(signal.reason ?? new DOMException('Aborted', 'AbortError')); }; signal?.addEventListener('abort', iptal, { once: true }); }); /** * Tek bir modele istek atar, yanıt metnini döndürür. * - Geçici 429'da artan beklemeyle AYNI modelle yeniden dener; 1113'te * beklemeden hata fırlatır (çağıran sıradaki modele geçer). * - Yer yalnız HTTP isteği sürerken tutulur; 429 beklemesinde bırakılır. * - `signal` iptal edilirse sırada beklerken bile istek atılmaz. */ export async function zaiChat(model, messages, opts = {}) { const { temperature = 0.2, json = true, maxTokens = 4000, signal, reasoningEffort, oncelik = 'yuksek', retries429 = Math.max(0, Number(process.env.ZAI_RETRIES_429 ?? 4) || 0), } = opts; const key = zaiKey(); if (!key) throw new ZaiError('ZAI_API_KEY yok'); for (let deneme = 0; ; deneme++) { signal?.throwIfAborted(); let durum = 0, govde = ''; await yerAl(oncelik); try { signal?.throwIfAborted(); const res = await fetch(zaiUrl(), { method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model, messages, temperature, max_tokens: maxTokens, stream: false, ...dusunmeAyari(model, reasoningEffort), ...(json ? { response_format: { type: 'json_object' } } : {}), }), signal, }); durum = res.status; govde = await res.text(); } finally { yerBirak(); } if (durum < 200 || durum >= 300) { const kisa = govde.slice(0, 200); if (durum === 429 && deneme < retries429 && gecici429(kisa)) { const taban = Math.max(0, Number(process.env.ZAI_RETRY_BASE_MS ?? 3000) || 0); await bekle(taban * (deneme + 1) + Math.floor(Math.random() * (taban / 3)), signal); continue; } throw new ZaiError(`${model}: HTTP ${durum} ${kisa}`, durum); } let mesaj; try { mesaj = JSON.parse(govde)?.choices?.[0]?.message; } catch { throw new ZaiError(`${model}: yanıt JSON değil`); } const icerik = typeof mesaj?.content === 'string' ? mesaj.content : ''; if (!icerik.trim()) throw new ZaiError(`${model}: boş içerik`); return icerik; } }