Files
ihaleist/server/zai.mjs
T
hikmettanandClaude Fable 5.1 9b3809e21b
Quality gates / test-build (push) Canceled after 0s
Quality gates / container (push) Canceled after 0s
fix(ted-ceviri): GLM harf bozulmasını onar, TED çevirisinde önce GLM 5.3
Canlı ölçüm (2026-09-11): glm-5.3-flash 22 başlığın 4'ünde kelime içi
büyük İ ve büyük harfe dönen kuyruk üretti ("Arazİ araçları",
"Elektrİklİ", "temİNİ"); açıklamalar temizdi.

- harfOnar: kelime küçük harfle devam ettikten sonra gelen büyükleri Türkçe
  kuralıyla küçültür; tamamı büyük kısaltmalara dokunmaz. Model çıktısına
  ve daha önce önbelleğe girmiş kayıtlara okunurken uygulanır.
- Çeviri prompt'una Türkçe yazım kuralı uyarısı eklendi.
- ZAI_TRANSLATE_MODELS varsayılanı glm-5.3 → glm-5.3-flash (paketler 6
  ilanlık, hız kaybı küçük; başlıklar kullanıcıya doğrudan görünüyor).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 00:26:33 +03:00

142 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}
}