Files
ihaleist/server/zai.mjs
T
hikmettanandClaude Fable 5.1 7fa71f1a4e
Quality gates / test-build (push) Canceled after 0s
Quality gates / container (push) Canceled after 0s
feat(ai): Z.ai GLM birincil sağlayıcı; OpenRouter yalnız yedek
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>
2026-09-10 23:11:41 +03:00

139 lines
6.2 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-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;
}
}