OpenRouter anahtarı iptal olup bakiye eksiye düşünce şartname analizi, TED çevirisi ve site taraması durmuştu. Artık ZAI_API_KEY ile GLM 5.3 ve GLM 5.3 Flash doğrudan Z.ai Coding Plan üzerinden çağrılır; kalan modeller OpenRouter anahtarı varsa yedek olarak denenir. - server/zai.mjs: doğrudan istemci; glm-5.x için reasoning_effort:low, glm-4.x için thinking kapalı; süreç genelinde en fazla 2 eşzamanlı istek (kullanıcı istekleri arka plan çevirisinden öncelikli); geçici 429'da aynı modelle yeniden deneme, 1113 bakiye hatasında hemen sıradaki model. - ai.mjs: 'zai' (GLM 5.3) sağlayıcısı eklendi, Z.ai yolu yeni istemciyi kullanıyor; varsayılan sıra zai → glmflash → gemini → … - ted-ceviri.mjs: önce ZAI_TRANSLATE_MODELS (glm-5.3-flash, glm-5.3), sonra OpenRouter; 2'li eşzamanlı parça işleme. - Admin model kataloğuna GLM 5.3 eklendi, kayıtlı zincirler sürüm 4'e taşınır. - tests/zai.test.mjs: düşünme ayarı, 429/1113 davranışı, eşzamanlılık, analyzeSpec ve TED çevirisinin Z.ai'ye gittiği. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
139 lines
6.2 KiB
JavaScript
139 lines
6.2 KiB
JavaScript
/**
|
||
* 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-flash,glm-5.3)
|
||
*/
|
||
|
||
const VARSAYILAN_URL = 'https://api.z.ai/api/paas/v4/chat/completions';
|
||
export const DEFAULT_ZAI_TRANSLATE_MODELS = ['glm-5.3-flash', 'glm-5.3'];
|
||
|
||
/** 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;
|
||
}
|
||
}
|