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>
This commit is contained in:
+6
-1
@@ -134,7 +134,12 @@ düğmesiyle metni çıkarılıp şartname analizine gidiyor. DT kayıtlarında
|
|||||||
|
|
||||||
- **Volume:** `/app/server/data` → `history.sqlite`, `dt.sqlite`, `users.sqlite`
|
- **Volume:** `/app/server/data` → `history.sqlite`, `dt.sqlite`, `users.sqlite`
|
||||||
- **Girilmiş sırlar:** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `RESEND_API_KEY`,
|
- **Girilmiş sırlar:** `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `RESEND_API_KEY`,
|
||||||
`OPENROUTER_API_KEY`, `GOOGLE_CLIENT_ID`
|
`ZAI_API_KEY`, `OPENROUTER_API_KEY` (yalnız yedek), `GOOGLE_CLIENT_ID`
|
||||||
|
- **Yapay zekâ (2026-09-10):** birincil sağlayıcı Z.ai Coding Plan. `ZAI_BASE_URL`
|
||||||
|
= `https://api.z.ai/api/coding/paas/v4/chat/completions`; aynı anda en fazla 2 istek
|
||||||
|
(`ZAI_MAX_CONCURRENCY`), GLM 5.x'te `reasoning_effort: low`. Şartname analizi
|
||||||
|
GLM 5.3 → GLM 5.3 Flash, TED çevirisi GLM 5.3 Flash → GLM 5.3; ikisi de bitince
|
||||||
|
OpenRouter zincirine düşer. Ayrıntı: `server/zai.mjs`.
|
||||||
- **E-posta:** `bildirim@ihaleist.com` (Resend, eu-west-1, doğrulanmış)
|
- **E-posta:** `bildirim@ihaleist.com` (Resend, eu-west-1, doğrulanmış)
|
||||||
- **Zamanlayıcı:** geçmiş tarama 6 sa · DT 4 sa · bildirim 6 sa
|
- **Zamanlayıcı:** geçmiş tarama 6 sa · DT 4 sa · bildirim 6 sa
|
||||||
|
|
||||||
|
|||||||
+25
-9
@@ -1,16 +1,20 @@
|
|||||||
import { analyzeInParts } from './analysis-parts.mjs';
|
import { analyzeInParts } from './analysis-parts.mjs';
|
||||||
import { qualifyAnalysis } from './analysis-quality.mjs';
|
import { qualifyAnalysis } from './analysis-quality.mjs';
|
||||||
|
import { zaiChat } from './zai.mjs';
|
||||||
/**
|
/**
|
||||||
* Gerçek AI şartname analizi — OpenRouter öncelikli, çoklu sağlayıcı fallback.
|
* Gerçek AI şartname analizi — Z.ai (GLM) birincil, çoklu sağlayıcı fallback.
|
||||||
* Bağımlılık yok (Node 18+ built-in fetch).
|
* Bağımlılık yok (Node 18+ built-in fetch).
|
||||||
*
|
*
|
||||||
* EN KOLAY KURULUM — tek anahtar, tüm modeller:
|
* BİRİNCİL KURULUM (2026-09'dan beri):
|
||||||
* OPENROUTER_API_KEY=sk-or-v1-...
|
* ZAI_API_KEY=... GLM 5.3 ve GLM 5.3 Flash doğrudan Z.ai'den
|
||||||
* Bu tanımlıysa zincirdeki tüm modeller OpenRouter üzerinden çağrılır.
|
* ZAI_BASE_URL=... Coding Plan ucu (bkz. zai.mjs)
|
||||||
*
|
*
|
||||||
* İsteğe bağlı doğrudan sağlayıcı anahtarları (OpenRouter'ı ezer):
|
* YEDEK — tek anahtar, kalan tüm modeller:
|
||||||
* ZAI_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY,
|
* OPENROUTER_API_KEY=sk-or-v1-...
|
||||||
* XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
|
* Doğrudan anahtarı olmayan sağlayıcılar OpenRouter üzerinden çağrılır.
|
||||||
|
*
|
||||||
|
* İsteğe bağlı diğer doğrudan anahtarlar (OpenRouter'ı ezer):
|
||||||
|
* DEEPSEEK_API_KEY, GEMINI_API_KEY, XAI_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY
|
||||||
*
|
*
|
||||||
* Model kimlikleri env ile değiştirilebilir (ör. GLM_MODEL, GLM_OR_MODEL).
|
* Model kimlikleri env ile değiştirilebilir (ör. GLM_MODEL, GLM_OR_MODEL).
|
||||||
*/
|
*/
|
||||||
@@ -18,11 +22,17 @@ import { qualifyAnalysis } from './analysis-quality.mjs';
|
|||||||
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
const OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';
|
||||||
|
|
||||||
export const PROVIDERS = {
|
export const PROVIDERS = {
|
||||||
|
zai: {
|
||||||
|
label: 'GLM 5.3', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
|
||||||
|
model: () => process.env.GLM_MODEL || 'glm-5.3',
|
||||||
|
or: () => process.env.GLM_OR_MODEL || '~z-ai/glm-latest',
|
||||||
|
style: 'zai',
|
||||||
|
},
|
||||||
glmflash: {
|
glmflash: {
|
||||||
label: 'GLM 5.3 Flash', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
|
label: 'GLM 5.3 Flash', provider: 'Z.ai', envKey: 'ZAI_API_KEY',
|
||||||
model: () => process.env.GLM_FLASH_MODEL || 'glm-5.3-flash',
|
model: () => process.env.GLM_FLASH_MODEL || 'glm-5.3-flash',
|
||||||
or: () => process.env.GLM_FLASH_OR_MODEL || 'z-ai/glm-5.3-flash',
|
or: () => process.env.GLM_FLASH_OR_MODEL || 'z-ai/glm-5.3-flash',
|
||||||
url: 'https://api.z.ai/api/paas/v4/chat/completions', style: 'openai',
|
style: 'zai',
|
||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
label: 'Gemini', provider: 'Google', envKey: 'GEMINI_API_KEY',
|
label: 'Gemini', provider: 'Google', envKey: 'GEMINI_API_KEY',
|
||||||
@@ -331,7 +341,7 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
|||||||
// modeller (ör. ücretsiz/limitli olanlar) tıkanınca analiz boş kalmasın;
|
// modeller (ör. ücretsiz/limitli olanlar) tıkanınca analiz boş kalmasın;
|
||||||
// yalnız üsttekilerin hepsi başarısız olursa devreye girer.
|
// yalnız üsttekilerin hepsi başarısız olursa devreye girer.
|
||||||
const YEDEK = 'gemini';
|
const YEDEK = 'gemini';
|
||||||
const secim = (Array.isArray(chain) && chain.length ? chain : ['glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
|
const secim = (Array.isArray(chain) && chain.length ? chain : ['zai', 'glmflash', 'gemini', 'deepseek', 'grok', 'gpt', 'claude'])
|
||||||
.filter((id) => PROVIDERS[id]);
|
.filter((id) => PROVIDERS[id]);
|
||||||
const order = secim.includes(YEDEK) ? secim : [...secim, YEDEK];
|
const order = secim.includes(YEDEK) ? secim : [...secim, YEDEK];
|
||||||
// Derin analiz hem daha çok metin okur hem daha uzun JSON üretir; çıktı
|
// Derin analiz hem daha çok metin okur hem daha uzun JSON üretir; çıktı
|
||||||
@@ -358,6 +368,12 @@ export async function analyzeSpec({ text, fileName, tender, chain, systemOverrid
|
|||||||
let raw;
|
let raw;
|
||||||
if (route.yol === 'openrouter') {
|
if (route.yol === 'openrouter') {
|
||||||
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem, maxTokens);
|
raw = await callOpenRouter(route.model, route.key, userPrompt, ac.signal, sistem, maxTokens);
|
||||||
|
} else if (p.style === 'zai') {
|
||||||
|
// Kullanıcı bekliyor: eşzamanlılık sırasında arka plan çevirisinin önüne geçer.
|
||||||
|
raw = await zaiChat(route.model, [
|
||||||
|
{ role: 'system', content: sistem },
|
||||||
|
{ role: 'user', content: userPrompt },
|
||||||
|
], { signal: ac.signal, maxTokens, temperature: 0.2, json: true, oncelik: 'yuksek' });
|
||||||
} else if (p.style === 'gemini') {
|
} else if (p.style === 'gemini') {
|
||||||
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
raw = await callGemini(p, route.key, route.model, userPrompt, ac.signal, sistem, maxTokens);
|
||||||
} else if (p.style === 'anthropic') {
|
} else if (p.style === 'anthropic') {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ KURALLAR:
|
|||||||
*/
|
*/
|
||||||
export async function scanSite(inputUrl) {
|
export async function scanSite(inputUrl) {
|
||||||
if (!aiConfigured()) {
|
if (!aiConfigured()) {
|
||||||
throw new Error('Site analizi için AI yapılandırılmamış (OPENROUTER_API_KEY gerekli).');
|
throw new Error('Site analizi için AI yapılandırılmamış (ZAI_API_KEY veya OPENROUTER_API_KEY gerekli).');
|
||||||
}
|
}
|
||||||
const url = normalizeUrl(inputUrl);
|
const url = normalizeUrl(inputUrl);
|
||||||
const ana = await fetchPage(url.href);
|
const ana = await fetchPage(url.href);
|
||||||
|
|||||||
+47
-26
@@ -1,7 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* TED ilan başlık/açıklamalarının Türkçesi.
|
* TED ilan başlık/açıklamalarının Türkçesi.
|
||||||
* 1) TED çok dilli alanında Türkçe varsa (tur) doğrudan o kullanılır — model gerekmez.
|
* 1) TED çok dilli alanında Türkçe varsa (tur) doğrudan o kullanılır — model gerekmez.
|
||||||
* 2) Yoksa OpenRouter ile toplu çevrilir (bir sayfa = tek istek), sonuç
|
* 2) Yoksa model ile toplu çevrilir (6 ilan = tek istek): önce Z.ai GLM
|
||||||
|
* (ZAI_TRANSLATE_MODELS), o da olmazsa OpenRouter. Sonuç
|
||||||
* server/data/ted.sqlite'ta ilan numarasına göre saklanır; ikinci görüntüleme ücretsizdir.
|
* server/data/ted.sqlite'ta ilan numarasına göre saklanır; ikinci görüntüleme ücretsizdir.
|
||||||
* Çeviri başarısız olursa özgün metin döner; liste asla bekletilmez (üst sınır ~40 sn).
|
* Çeviri başarısız olursa özgün metin döner; liste asla bekletilmez (üst sınır ~40 sn).
|
||||||
*/
|
*/
|
||||||
@@ -9,6 +10,7 @@ import path from 'node:path';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { openRouterKey } from './ai.mjs';
|
import { openRouterKey } from './ai.mjs';
|
||||||
|
import { zaiKey, zaiChat, zaiTranslateModels } from './zai.mjs';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const DATA_DIR = path.join(__dirname, 'data');
|
const DATA_DIR = path.join(__dirname, 'data');
|
||||||
@@ -44,33 +46,51 @@ function yaz(ikn, baslik_tr, aciklama_tr, kaynak) {
|
|||||||
else bellek.set(ikn, { baslik_tr, aciklama_tr });
|
else bellek.set(ikn, { baslik_tr, aciklama_tr });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ölçüm (6 ilan): deepseek-v4-flash 10 sn/851 token; glm-5.3-flash 71 sn boyunca
|
// OpenRouter yedek sırası (yalnız Z.ai başarısız olursa ve OPENROUTER_API_KEY
|
||||||
// 3000 "düşünme" tokenı tüketip boş dönüyor; gemini-flash-latest OpenRouter'da
|
// varsa). Ölçüm (6 ilan, OpenRouter): deepseek-v4-flash 10 sn/851 token;
|
||||||
// geçersiz kimlik. Bu yüzden sıra: deepseek → gemini 3.7 flash → glm.
|
// glm-5.3-flash OpenRouter üzerinden 71 sn düşünüp boş dönüyor (doğrudan
|
||||||
const MODELLER = ['deepseek/deepseek-v4-flash', 'google/gemini-3.7-flash', 'z-ai/glm-5.3-flash'];
|
// Z.ai'de reasoning_effort:low ile bu sorun yok, bkz. zai.mjs).
|
||||||
|
const OR_MODELLER = ['deepseek/deepseek-v4-flash', 'google/gemini-3.7-flash', 'z-ai/glm-5.3-flash'];
|
||||||
const SISTEM = 'Sen kamu ihale terminolojisine hâkim bir çevirmensin. Verilen AB (TED) ihale başlık ve açıklamalarını doğal, resmî Türkçeye çevir. Başlık "Ülke – Konu – İlan adı" biçimindedir: ülke adını da Türkçeleştir (Austria→Avusturya). Kurum adlarını çevirme, sayıları/tarihleri aynen koru. Yalnızca JSON döndür: {"ceviriler":[{"id":"...","baslik":"...","aciklama":"..."}]} — id alanını aynen kopyala; açıklama yoksa boş string.';
|
const SISTEM = 'Sen kamu ihale terminolojisine hâkim bir çevirmensin. Verilen AB (TED) ihale başlık ve açıklamalarını doğal, resmî Türkçeye çevir. Başlık "Ülke – Konu – İlan adı" biçimindedir: ülke adını da Türkçeleştir (Austria→Avusturya). Kurum adlarını çevirme, sayıları/tarihleri aynen koru. Yalnızca JSON döndür: {"ceviriler":[{"id":"...","baslik":"...","aciklama":"..."}]} — id alanını aynen kopyala; açıklama yoksa boş string.';
|
||||||
|
|
||||||
|
/** Çeviri yapılabilir mi: Z.ai ya da OpenRouter anahtarından biri yeter. */
|
||||||
|
export const ceviriMumkun = () => Boolean(zaiKey() || openRouterKey());
|
||||||
|
|
||||||
|
function cevirileriAyikla(icerik) {
|
||||||
|
const s = icerik.slice(icerik.indexOf('{'), icerik.lastIndexOf('}') + 1);
|
||||||
|
const out = JSON.parse(s);
|
||||||
|
if (!Array.isArray(out?.ceviriler)) throw new Error('beklenen JSON değil');
|
||||||
|
return out.ceviriler;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openRouterdanCevir(model, key, mesajlar, signal) {
|
||||||
|
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
|
method: 'POST', signal,
|
||||||
|
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://ihaleist.com', 'X-Title': 'ihaleist' },
|
||||||
|
body: JSON.stringify({ model, temperature: 0.1, max_tokens: 2500, response_format: { type: 'json_object' }, messages: mesajlar }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return data?.choices?.[0]?.message?.content || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sırayla dener: Z.ai modelleri → OpenRouter modelleri. İlk geçerli JSON döner. */
|
||||||
async function modeldenCevir(kalemler, signal) {
|
async function modeldenCevir(kalemler, signal) {
|
||||||
const key = openRouterKey();
|
if (!ceviriMumkun()) throw new Error('ZAI_API_KEY veya OPENROUTER_API_KEY yok');
|
||||||
if (!key) throw new Error('OPENROUTER_API_KEY yok');
|
|
||||||
const girdi = JSON.stringify({ ilanlar: kalemler.map((k) => ({ id: k.id, baslik: k.baslik, aciklama: (k.aciklama || '').slice(0, 600) })) });
|
const girdi = JSON.stringify({ ilanlar: kalemler.map((k) => ({ id: k.id, baslik: k.baslik, aciklama: (k.aciklama || '').slice(0, 600) })) });
|
||||||
|
const mesajlar = [{ role: 'system', content: SISTEM }, { role: 'user', content: girdi }];
|
||||||
|
const adimlar = [];
|
||||||
|
if (zaiKey()) for (const model of zaiTranslateModels()) {
|
||||||
|
// Arka plan işi: kullanıcının beklediği analiz istekleri sırada öne geçer.
|
||||||
|
adimlar.push(() => zaiChat(model, mesajlar, { signal, temperature: 0.1, maxTokens: 2500, json: true, oncelik: 'dusuk' }));
|
||||||
|
}
|
||||||
|
if (openRouterKey()) for (const model of OR_MODELLER) {
|
||||||
|
adimlar.push(() => openRouterdanCevir(model, openRouterKey(), mesajlar, signal));
|
||||||
|
}
|
||||||
let sonHata = null;
|
let sonHata = null;
|
||||||
for (const model of MODELLER) {
|
for (const adim of adimlar) {
|
||||||
try {
|
signal?.throwIfAborted();
|
||||||
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
try { return cevirileriAyikla(await adim()); } catch (e) { if (e?.name === 'AbortError') throw e; sonHata = e; }
|
||||||
method: 'POST', signal,
|
|
||||||
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://ihaleist.com', 'X-Title': 'ihaleist' },
|
|
||||||
body: JSON.stringify({ model, temperature: 0.1, max_tokens: 2500, response_format: { type: 'json_object' },
|
|
||||||
messages: [{ role: 'system', content: SISTEM }, { role: 'user', content: girdi }] }),
|
|
||||||
});
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
||||||
const icerik = data?.choices?.[0]?.message?.content || '';
|
|
||||||
const s = icerik.slice(icerik.indexOf('{'), icerik.lastIndexOf('}') + 1);
|
|
||||||
const out = JSON.parse(s);
|
|
||||||
if (!Array.isArray(out?.ceviriler)) throw new Error('beklenen JSON değil');
|
|
||||||
return out.ceviriler;
|
|
||||||
} catch (e) { sonHata = e; }
|
|
||||||
}
|
}
|
||||||
throw sonHata || new Error('çeviri başarısız');
|
throw sonHata || new Error('çeviri başarısız');
|
||||||
}
|
}
|
||||||
@@ -111,12 +131,13 @@ export async function tedCevir(tenders, { blockMs = 12000 } = {}) {
|
|||||||
if (c && c.baslik_tr) { t.baslikTr = c.baslik_tr; t.aciklamaTr = c.aciklama_tr || null; t.ceviriKaynagi = 'onbellek'; continue; }
|
if (c && c.baslik_tr) { t.baslikTr = c.baslik_tr; t.aciklamaTr = c.aciklama_tr || null; t.ceviriKaynagi = 'onbellek'; continue; }
|
||||||
if (!devamEden.has(t.ikn)) bekleyen.push(t);
|
if (!devamEden.has(t.ikn)) bekleyen.push(t);
|
||||||
}
|
}
|
||||||
if (!openRouterKey()) return 0;
|
if (!ceviriMumkun()) return 0;
|
||||||
if (!bekleyen.length) return tenders.filter((t) => !t.baslikTr).length;
|
if (!bekleyen.length) return tenders.filter((t) => !t.baslikTr).length;
|
||||||
for (const t of bekleyen) devamEden.add(t.ikn);
|
for (const t of bekleyen) devamEden.add(t.ikn);
|
||||||
const parcalar = [];
|
const parcalar = [];
|
||||||
for (let i = 0; i < bekleyen.length; i += 6) parcalar.push(bekleyen.slice(i, i + 6));
|
for (let i = 0; i < bekleyen.length; i += 6) parcalar.push(bekleyen.slice(i, i + 6));
|
||||||
// Parçalar 3'lü eşzamanlı işlenir: 30 ilanlık liste ~5 yerine ~2 model turunda biter.
|
// Parçalar 2'li eşzamanlı işlenir: Z.ai Coding Plan aynı anda en fazla 2
|
||||||
|
// isteğe izin veriyor; fazlası zaten zai.mjs'teki sınırda beklerdi.
|
||||||
const is = (async () => {
|
const is = (async () => {
|
||||||
const kuyruk = [...parcalar];
|
const kuyruk = [...parcalar];
|
||||||
const isci = async () => {
|
const isci = async () => {
|
||||||
@@ -125,7 +146,7 @@ export async function tedCevir(tenders, { blockMs = 12000 } = {}) {
|
|||||||
try { await parcayiCevir(parca); } catch (e) { console.warn('[ted-ceviri] parça atlandı:', e.message); }
|
try { await parcayiCevir(parca); } catch (e) { console.warn('[ted-ceviri] parça atlandı:', e.message); }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await Promise.all(Array.from({ length: Math.min(3, parcalar.length) }, isci));
|
await Promise.all(Array.from({ length: Math.min(2, parcalar.length) }, isci));
|
||||||
})();
|
})();
|
||||||
await Promise.race([is, new Promise((r) => setTimeout(r, blockMs))]);
|
await Promise.race([is, new Promise((r) => setTimeout(r, blockMs))]);
|
||||||
return tenders.filter((t) => !t.baslikTr).length;
|
return tenders.filter((t) => !t.baslikTr).length;
|
||||||
|
|||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ export const AiModelSettings = () => {
|
|||||||
<Zap className="w-4 h-4 text-emerald-700 shrink-0 mt-0.5" />
|
<Zap className="w-4 h-4 text-emerald-700 shrink-0 mt-0.5" />
|
||||||
<div className="text-xs text-emerald-900 leading-relaxed">
|
<div className="text-xs text-emerald-900 leading-relaxed">
|
||||||
<strong>AI aktif</strong> — {cfg.providers.length} model kullanılabilir
|
<strong>AI aktif</strong> — {cfg.providers.length} model kullanılabilir
|
||||||
{aktifYol === 'openrouter' ? ' (OpenRouter üzerinden tek anahtarla)' : ' (doğrudan sağlayıcı anahtarlarıyla)'}.
|
{aktifYol === 'openrouter' ? ' (OpenRouter üzerinden tek anahtarla)' : ' (birincil sağlayıcı doğrudan, kalanlar yedek)'}.
|
||||||
<div className="font-mono text-[11px] mt-1 text-emerald-800">
|
<div className="font-mono text-[11px] mt-1 text-emerald-800">
|
||||||
{cfg.providers.map((p) => `${p.label}: ${p.model}`).join(' · ')}
|
{cfg.providers.map((p) => `${p.label}: ${p.model}`).join(' · ')}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,9 +80,9 @@ export const AiModelSettings = () => {
|
|||||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 flex gap-3">
|
||||||
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
|
<AlertTriangle className="w-4 h-4 text-amber-700 shrink-0 mt-0.5" />
|
||||||
<p className="text-xs text-amber-900 leading-relaxed">
|
<p className="text-xs text-amber-900 leading-relaxed">
|
||||||
<strong>AI anahtarı yok.</strong> Sunucuya tek bir <code className="bg-amber-100 px-1 rounded">OPENROUTER_API_KEY</code> ekleyin —
|
<strong>AI anahtarı yok.</strong> Sunucuya <code className="bg-amber-100 px-1 rounded">ZAI_API_KEY</code> (birincil, GLM modelleri)
|
||||||
aşağıdaki tüm modeller bu tek anahtarla çalışır. Aşağıdaki sıralama yalnızca tercih kaydeder;
|
ve isteğe bağlı yedek <code className="bg-amber-100 px-1 rounded">OPENROUTER_API_KEY</code> ekleyin.
|
||||||
çağrılar anahtar eklendiğinde başlar.
|
Aşağıdaki sıralama yalnızca tercih kaydeder; çağrılar anahtar eklendiğinde başlar.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ export const AppProvider = ({ children }) => {
|
|||||||
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
// AI Model Yönetimi (admin panelden seçilebilir + fallback zinciri)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const AI_MODEL_CATALOG = [
|
const AI_MODEL_CATALOG = [
|
||||||
|
{ id: 'zai', label: 'GLM 5.3', provider: 'Z.ai', or: '~z-ai/glm-latest', model: 'glm-5.3', renk: 'blue' },
|
||||||
{ id: 'glmflash', label: 'GLM 5.3 Flash', provider: 'Z.ai', or: 'z-ai/glm-5.3-flash', model: 'glm-5.3-flash', renk: 'blue' },
|
{ id: 'glmflash', label: 'GLM 5.3 Flash', provider: 'Z.ai', or: 'z-ai/glm-5.3-flash', model: 'glm-5.3-flash', renk: 'blue' },
|
||||||
{ id: 'gemini', label: 'Gemini Flash Latest', provider: 'Google', or: '~google/gemini-flash-latest', model: 'gemini-flash-latest', renk: 'amber' },
|
{ id: 'gemini', label: 'Gemini Flash Latest', provider: 'Google', or: '~google/gemini-flash-latest', model: 'gemini-flash-latest', renk: 'amber' },
|
||||||
{ id: 'deepseek', label: 'DeepSeek V4 Flash Latest', provider: 'DeepSeek', or: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-chat', renk: 'indigo' },
|
{ id: 'deepseek', label: 'DeepSeek V4 Flash Latest', provider: 'DeepSeek', or: '~deepseek/deepseek-v4-flash-latest', model: 'deepseek-chat', renk: 'indigo' },
|
||||||
@@ -473,21 +474,21 @@ export const AppProvider = ({ children }) => {
|
|||||||
{ id: 'claude', label: 'Claude Opus Latest', provider: 'Anthropic', or: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
{ id: 'claude', label: 'Claude Opus Latest', provider: 'Anthropic', or: '~anthropic/claude-opus-latest', model: 'claude-opus-5', renk: 'orange' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Varsayılan fallback zinciri: GLM 5.3 Flash (birincil) → Gemini → DeepSeek
|
// Varsayılan fallback zinciri (2026-09-10, Z.ai birincil): GLM 5.3 → GLM 5.3 Flash
|
||||||
// (glm-latest kaldırıldı: birincil ile aynı modele çözünüyordu, gerçek yedek değildi)
|
// → Gemini → DeepSeek. İlk ikisi doğrudan Z.ai'den, kalanlar OpenRouter yedeği.
|
||||||
const VARSAYILAN_ZINCIR = ['glmflash', 'gemini', 'deepseek'];
|
const VARSAYILAN_ZINCIR = ['zai', 'glmflash', 'gemini', 'deepseek'];
|
||||||
const ZINCIR_SURUM = 3; // artırıldığında kayıtlı zincir yeni düzene taşınır
|
const ZINCIR_SURUM = 4; // artırıldığında kayıtlı zincir yeni düzene taşınır
|
||||||
|
|
||||||
const aiModelAnahtari = (email) => `ihaleist_ai_model_config:${encodeURIComponent(String(email || '').toLowerCase())}`;
|
const aiModelAnahtari = (email) => `ihaleist_ai_model_config:${encodeURIComponent(String(email || '').toLowerCase())}`;
|
||||||
const normalizeAiModelConfig = (saved) => {
|
const normalizeAiModelConfig = (saved) => {
|
||||||
if (saved) {
|
if (saved) {
|
||||||
try {
|
try {
|
||||||
const cfg = JSON.parse(saved);
|
const cfg = JSON.parse(saved);
|
||||||
// Geçiş: kayıtlı zinciri yeni düzene taşı (glm çıkar, glmflash → gemini başa)
|
// Geçiş: kayıtlı zinciri yeni düzene taşı (Z.ai GLM 5.3 ve Flash başa, eski glm çıkar)
|
||||||
if ((cfg.surum || 1) < ZINCIR_SURUM) {
|
if ((cfg.surum || 1) < ZINCIR_SURUM) {
|
||||||
const chain = Array.isArray(cfg.chain) ? cfg.chain : [];
|
const chain = Array.isArray(cfg.chain) ? cfg.chain : [];
|
||||||
const kalan = chain.filter((id) => !['glmflash', 'gemini', 'glm'].includes(id));
|
const kalan = chain.filter((id) => !['zai', 'glmflash', 'gemini', 'glm'].includes(id));
|
||||||
return { ...cfg, surum: ZINCIR_SURUM, chain: ['glmflash', 'gemini', ...kalan] };
|
return { ...cfg, surum: ZINCIR_SURUM, chain: ['zai', 'glmflash', 'gemini', ...kalan] };
|
||||||
}
|
}
|
||||||
// Katalogdan çıkarılmış modelleri kayıtlı zincirden ayıkla
|
// Katalogdan çıkarılmış modelleri kayıtlı zincirden ayıkla
|
||||||
if (Array.isArray(cfg.chain)) {
|
if (Array.isArray(cfg.chain)) {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { dusunmeAyari, zaiChat, gecici429, zaiAktifIstek } from '../server/zai.mjs';
|
||||||
|
import { analyzeSpec, routeFor } from '../server/ai.mjs';
|
||||||
|
import { tedCevir } from '../server/ted-ceviri.mjs';
|
||||||
|
|
||||||
|
const ZAI_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
|
||||||
|
const ok = (content) => new Response(JSON.stringify({ choices: [{ message: { content } }] }), { status: 200 });
|
||||||
|
const hata429 = (code) => new Response(JSON.stringify({ error: { code, message: 'x' } }), { status: 429 });
|
||||||
|
|
||||||
|
function ortam(vars) {
|
||||||
|
const eski = {};
|
||||||
|
for (const [k, v] of Object.entries(vars)) { eski[k] = process.env[k]; if (v === undefined) delete process.env[k]; else process.env[k] = v; }
|
||||||
|
return () => { for (const [k, v] of Object.entries(eski)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; } };
|
||||||
|
}
|
||||||
|
const TEMEL = { ZAI_API_KEY: 'synthetic-test-only', ZAI_BASE_URL: ZAI_URL, ZAI_RETRY_BASE_MS: '0', ZAI_MAX_CONCURRENCY: '2', OPENROUTER_API_KEY: undefined, GEMINI_API_KEY: undefined };
|
||||||
|
|
||||||
|
test('düşünme ayarı modele göre: glm-5 reasoning_effort low, glm-4 thinking kapalı', () => {
|
||||||
|
const geri = ortam({ ZAI_REASONING_EFFORT: undefined });
|
||||||
|
try {
|
||||||
|
assert.deepEqual(dusunmeAyari('glm-5.3'), { reasoning_effort: 'low' });
|
||||||
|
assert.deepEqual(dusunmeAyari('glm-5.3-flash'), { reasoning_effort: 'low' });
|
||||||
|
assert.deepEqual(dusunmeAyari('glm-5.3', null), {});
|
||||||
|
assert.deepEqual(dusunmeAyari('glm-4.7-flash'), { thinking: { type: 'disabled' } });
|
||||||
|
} finally { geri(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('geçici 429 aynı modelle yeniden denenir, bakiye hatası (1113) hemen fırlatılır', async () => {
|
||||||
|
const geri = ortam(TEMEL); const orijinal = globalThis.fetch;
|
||||||
|
try {
|
||||||
|
const govdeler = [];
|
||||||
|
globalThis.fetch = async (url, { body }) => { govdeler.push(JSON.parse(body)); return govdeler.length === 1 ? hata429('1302') : ok('{"a":1}'); };
|
||||||
|
const sonuc = await zaiChat('glm-5.3-flash', [{ role: 'user', content: 'x' }], { retries429: 2 });
|
||||||
|
assert.equal(sonuc, '{"a":1}');
|
||||||
|
assert.equal(govdeler.length, 2);
|
||||||
|
assert.equal(govdeler[1].model, 'glm-5.3-flash');
|
||||||
|
assert.equal(govdeler[1].reasoning_effort, 'low');
|
||||||
|
assert.deepEqual(govdeler[1].response_format, { type: 'json_object' });
|
||||||
|
|
||||||
|
let cagri = 0;
|
||||||
|
globalThis.fetch = async () => { cagri++; return hata429('1113'); };
|
||||||
|
await assert.rejects(zaiChat('glm-5.3', [{ role: 'user', content: 'x' }], { retries429: 3 }), /1113/);
|
||||||
|
assert.equal(cagri, 1);
|
||||||
|
assert.equal(gecici429('{"error":{"code":"1302"}}'), true);
|
||||||
|
} finally { globalThis.fetch = orijinal; geri(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('eşzamanlı Z.ai isteği sınırı süreç genelinde 2 ile sınırlı', async () => {
|
||||||
|
const geri = ortam(TEMEL); const orijinal = globalThis.fetch;
|
||||||
|
try {
|
||||||
|
let acik = 0, tepe = 0;
|
||||||
|
globalThis.fetch = async () => { acik++; tepe = Math.max(tepe, acik); await new Promise((r) => setTimeout(r, 20)); acik--; return ok('{}'); };
|
||||||
|
await Promise.all([1, 2, 3, 4].map(() => zaiChat('glm-5.3-flash', [{ role: 'user', content: 'x' }])));
|
||||||
|
assert.equal(tepe, 2);
|
||||||
|
assert.equal(zaiAktifIstek(), 0);
|
||||||
|
} finally { globalThis.fetch = orijinal; geri(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('analyzeSpec Z.ai anahtarı varken GLM 5.3\'ü doğrudan Z.ai\'den çağırır', async () => {
|
||||||
|
const geri = ortam(TEMEL); const orijinal = globalThis.fetch;
|
||||||
|
try {
|
||||||
|
assert.deepEqual(routeFor('zai'), { yol: 'direct', key: 'synthetic-test-only', model: 'glm-5.3' });
|
||||||
|
const istekler = [];
|
||||||
|
globalThis.fetch = async (url, { body }) => { istekler.push({ url, body: JSON.parse(body) }); return ok(JSON.stringify({ summary: 'özet', qualificationCriteria: [], criticalRequirements: [], risksAndPenalties: [], bidAnalysis: {} })); };
|
||||||
|
const out = await analyzeSpec({ text: 'Sentetik şartname metni', chain: ['zai'] });
|
||||||
|
assert.equal(istekler.length, 1);
|
||||||
|
assert.equal(istekler[0].url, ZAI_URL);
|
||||||
|
assert.equal(istekler[0].body.model, 'glm-5.3');
|
||||||
|
assert.equal(istekler[0].body.reasoning_effort, 'low');
|
||||||
|
assert.equal(out.model.provider, 'Z.ai');
|
||||||
|
assert.equal(out.model.yol, 'direct');
|
||||||
|
assert.equal(out.result.summary, 'özet');
|
||||||
|
} finally { globalThis.fetch = orijinal; geri(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
test('TED çevirisi OpenRouter anahtarı olmadan Z.ai GLM 5.3 Flash ile çalışır', async () => {
|
||||||
|
const geri = ortam(TEMEL); const orijinal = globalThis.fetch;
|
||||||
|
try {
|
||||||
|
const istekler = [];
|
||||||
|
globalThis.fetch = async (url, { body }) => {
|
||||||
|
const b = JSON.parse(body); istekler.push({ url, model: b.model });
|
||||||
|
const ilanlar = JSON.parse(b.messages[1].content).ilanlar;
|
||||||
|
return ok(JSON.stringify({ ceviriler: ilanlar.map((i) => ({ id: i.id, baslik: `TR ${i.baslik}`, aciklama: '' })) }));
|
||||||
|
};
|
||||||
|
const tenders = [{ id: 't1', baslik: 'Austria – Works' }, { id: 't2', baslik: 'Poland – Supplies' }];
|
||||||
|
const bekleyen = await tedCevir(tenders, { blockMs: 2000 });
|
||||||
|
assert.equal(bekleyen, 0);
|
||||||
|
assert.equal(istekler.length, 1);
|
||||||
|
assert.equal(istekler[0].url, ZAI_URL);
|
||||||
|
assert.equal(istekler[0].model, 'glm-5.3-flash');
|
||||||
|
assert.equal(tenders[0].baslikTr, 'TR Austria – Works');
|
||||||
|
assert.equal(tenders[1].ceviriKaynagi, 'model');
|
||||||
|
} finally { globalThis.fetch = orijinal; geri(); }
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user