/** * Danışman sohbeti eval'i — gerçek danismanAkisi (11 araç) ile. PARA HARCAR. * * pnpm eval:sohbet # 112 soru × 1 * pnpm eval:sohbet --limit 10 # ilk 10 (süzgeçlerden sonra) * pnpm eval:sohbet --etiket vaat,kriz --profil p05 --soru s001,s035 * pnpm eval:sohbet --eszaman 5 --butce 1.5 --liste scripts/eval/sonuc/.json * pnpm eval:sohbet --etiket vaat,baraj --tekrar 3 # her soru 3 kez (model stokastik) * * Bağlam route.ts'in kurduğu biçimde kurulur: sistemPromptu + listeOzetiCikar * + sohbetListeOzeti + modelPenceresi + aracBaglami aynı modüllerden import * edilir; kopya yok. Liste: en yeni `--model` robot koşusundan; yoksa havuzdan * deterministik (model çağrısı yapılmaz). * * profil_guncelle gerçek kodla çalışır ama data/app.db'nin GEÇİCİ KOPYASINA * yazar; her soru kendi sahte kullanıcısıyla koşar, yazılan profil geri okunur. */ import { readFileSync } from "node:fs"; import Database from "better-sqlite3"; import { argOku, dayanaksizSayilar, evalAppDbHazirla, genelKarar, gitDurumu, havuzdaKos, icerikKontrolu, jevKontrolleri, jevMaliyeti, jevSor, JEV_TOPLAM, kisalt, olcerekKos, robotSonuclari, sonucYaz, tablo, TOPLAM, vaatIfadeleri, yuzdelik, type JevSonuc, type Karar, type Kontrol, } from "./ortak"; import { JEV_DANISMAN_ESIKLERI, JEV_EVAL_MODEL, JEV_SORU_GRUPLARI, type JevDanismanState, type JevSoruAdi } from "./jev-sorulari"; import { PROFILLER, SORULAR, type EvalProfil, type EvalSoru } from "./profiller"; import type { RaporParams, RaporSonuc } from "@/features/rapor/types/rapor"; const paramsYap = (p: EvalProfil): RaporParams => ({ sira: p.sihirbaz.sira, tur: p.sihirbaz.tur, ...p.sihirbaz.secimler }); type CanliDenetim = import("@/lib/ai/sohbet-akisi").CiktiDenetimi; type AracCagrisi = { ad: string; girdi: unknown; cikti: unknown; hata?: string }; /** danisman-prompt.ts'in kilitli kullanıcıya eklediği ürün bilgisinin hakem bağlamındaki karşılığı. */ const KILITLI_URUN_BILGISI = "Ürün bilgisi: adayın paketi yok; yalnız ilk tercihler açık, tam liste Tercih Dönemi Paketi ile (299 TL, /paket) açılır."; async function main() { // Ortam ÖNCE: appdb modülü import anında bağlanır const dbYolu = evalAppDbHazirla(); const sqlite = new Database(dbYolu); const { danismanAkisi } = await import("@/lib/ai/sohbet-akisi"); const { sistemPromptu, sohbetListeOzeti } = await import("@/lib/ai/danisman-prompt"); const { modelPenceresi } = await import("@/lib/ai/sohbet-gecmisi"); const { barajTablosu } = await import("@/lib/ai/araclar"); const { listeOzetiCikar } = await import("@/lib/ai/rapor"); const { sohbetModeli, saglayiciAdi, anahtarVar } = await import("@/lib/ai/client"); const { JEV_DUZELTME_NOTU } = await import("@/lib/ai/jev"); const { vaatDiliBul, KAYIT_DUZELTME_NOTU } = await import("@/lib/ai/cikti-denetimi"); const { havuzOlustur, dilimHedefiHesapla } = await import("@/lib/rapor-havuzu"); const { PUAN_TURLERI } = await import("@/types/yokatlas"); const { SON_YIL } = await import("@/lib/veri-yillari"); if (!anahtarVar()) { console.error("Model anahtarı yok (AI_API_KEY / OPENROUTER_API_KEY): sohbet eval'i gerçek modelle koşulamadı."); process.exit(2); } // ── soru seçimi ── const liste = (ad: string) => argOku(ad)?.split(",").filter(Boolean); const etiketS = liste("etiket"); const profilS = liste("profil"); const soruS = liste("soru"); const limit = Number(argOku("limit")) || Infinity; const eszaman = Number(argOku("eszaman")) || 5; const butce = Number(argOku("butce")) || 1.5; const tekrar = Math.max(1, Math.floor(Number(argOku("tekrar")) || 1)); const sorular = SORULAR.filter( (s) => (!etiketS || s.etiketler.some((e) => etiketS.includes(e))) && (!profilS || profilS.some((p) => s.profilId.startsWith(p))) && (!soruS || soruS.includes(s.id)), ).slice(0, limit); // ── liste bağlamı ── // --liste verilmezse: gerçek model listesi içeren en yeni robot sonucu const robotListeleri = new Map(); const robotOku = (yol: string) => { const j = JSON.parse(readFileSync(yol, "utf8")) as { model?: { profiller?: { id: string; liste: RaporSonuc | null }[] } }; for (const p of j.model?.profiller ?? []) if (p.liste && !p.liste.devMock) robotListeleri.set(p.id, p.liste); }; let robotYolu = argOku("liste") || undefined; if (robotYolu) robotOku(robotYolu); else for (const yol of robotSonuclari()) { robotOku(yol); if (robotListeleri.size) { robotYolu = yol; break; } } const listeKaynagi = new Map(); const raporlar = new Map(); const raporGetir = (p: EvalProfil): RaporSonuc => { const var_ = raporlar.get(p.id); if (var_) return var_; let r = robotListeleri.get(p.id); listeKaynagi.set(p.id, r ? "robot" : "deterministik"); if (!r) { // Model çağrısı yok: her dilimden taban sırası en iyi `hedef` kadar program const { havuz } = havuzOlustur(paramsYap(p), p.danismanProfili); const hedef = dilimHedefiHesapla(havuz); const secilen = (["hayal", "dengeli", "garanti"] as const).flatMap((d) => havuz.filter((x) => x.dilim === d).sort((a, b) => (a.efektifSira ?? 0) - (b.efektifSira ?? 0)).slice(0, hedef[d]), ); r = { tercihler: secilen.map((x, i) => ({ programId: x.id, sira: i + 1, dilim: x.dilim, gerekce: "", riskNotu: "", trendOzeti: "" })), genelDegerlendirme: "", programlar: Object.fromEntries(secilen.map((x) => [x.id, { isim: x.isim, universite: x.universite, il: x.il, unitur: x.unitur, efektifSira: x.efektifSira }])), } as RaporSonuc; } raporlar.set(p.id, r); return r; }; const barajBaglami = `Kural bilgisi (ÖSYM başarı sırası barajları; aday sırası barajdan kötüyse o bölüm tercih edilemez): ${barajTablosu().map((b) => `${b.sira.toLocaleString("tr-TR")} = ${b.bolumler}`).join("; ")}`; const now = Date.now(); const kullaniciEkle = sqlite.prepare("INSERT OR REPLACE INTO user (id, name, email, email_verified, credit_balance, has_paket, created_at, updated_at) VALUES (?, 'Eval', ?, 1, 0, 1, ?, ?)"); const profilEkle = sqlite.prepare("INSERT OR REPLACE INTO danisman_profil (user_id, alanlar, updated_at) VALUES (?, ?, ?)"); const profilOku = sqlite.prepare("SELECT alanlar FROM danisman_profil WHERE user_id = ?"); console.log(`\n== Sohbet eval — ${sorular.length} soru${tekrar > 1 ? ` × ${tekrar} tekrar` : ""}, model ${sohbetModeli()} @ ${saglayiciAdi()}, eşzamanlılık ${eszaman}, bütçe ${butce} $ ==`); console.log(`Liste bağlamı: ${robotYolu && robotListeleri.size ? `${robotListeleri.size} profil ${robotYolu.split("/").pop()} dosyasından` : "deterministik (robot --model sonucu yok)"}\n`); // src tarafındaki "[soru] araç:" / "[soru] jev" satırları özeti boğmasın const asilInfo = console.info; console.info = () => {}; async function soruKos({ s, tekrarNo }: { s: EvalSoru; tekrarNo: number }) { const p = PROFILLER.find((x) => x.id === s.profilId); if (!p) throw new Error(`Profil yok: ${s.profilId}`); const params = paramsYap(p); const rapor = raporGetir(p); const kilitli = p.paketli === false; const tamOzet = listeOzetiCikar(rapor, params.sira, { kimlik: true }); const listeOzeti = sohbetListeOzeti(tamOzet, kilitli); const profil = { ...(p.danismanProfili ?? {}) }; const system = sistemPromptu({ params, listeOzeti, kilitli, profil, barajlar: barajTablosu() }); const gecmis = (s.gecmis ?? []).map((m) => ({ role: m.rol, content: m.icerik })); const mesajlar = modelPenceresi(gecmis, s.soru); const userId = `eval-${process.pid}-${s.id}-${tekrarNo}`; kullaniciEkle.run(userId, `${userId}@eval.invalid`, now, now); if (p.danismanProfili) profilEkle.run(userId, JSON.stringify(p.danismanProfili), now); const araclar: AracCagrisi[] = []; const girdiler = new Map(); let adim = 0; let ilkTokenMs: number | null = null; let tamMetin: string | null = null; let yarimMetin: string | null = null; let akisHatasi: string | null = null; let canliDenetim: CanliDenetim | null = null; const t0 = Date.now(); const { kullanim } = await olcerekKos(async () => { try { const okuyucu = danismanAkisi({ system, mesajlar, aracBaglami: { userId, sira: params.sira, tur: params.tur, profil }, onTamam: async (m) => { tamMetin = m; }, onYarim: async (m) => { yarimMetin = m; }, onDenetim: (d) => { canliDenetim = d; }, }).getReader(); for (;;) { const { done, value: parca } = await okuyucu.read(); if (done) break; const v = parca as { type: string; toolCallId?: string; toolName?: string; input?: unknown; output?: unknown; errorText?: string }; if (v.type === "start-step") adim++; else if (v.type === "text-delta" && ilkTokenMs == null) ilkTokenMs = Date.now() - t0; else if (v.type === "tool-input-available") { const c: AracCagrisi = { ad: v.toolName ?? "?", girdi: v.input, cikti: undefined }; araclar.push(c); if (v.toolCallId) girdiler.set(v.toolCallId, c); } else if (v.type === "tool-output-available" && v.toolCallId) { const c = girdiler.get(v.toolCallId); if (c) c.cikti = v.output; } else if (v.type === "tool-input-error" || v.type === "tool-output-error") { const c = v.toolCallId ? girdiler.get(v.toolCallId) : undefined; if (c) c.hata = v.errorText ?? v.type; else araclar.push({ ad: v.toolName ?? "?", girdi: v.input, cikti: undefined, hata: v.errorText ?? v.type }); } else if (v.type === "error") akisHatasi = v.errorText ?? "hata"; } } catch (e) { akisHatasi = e instanceof Error ? e.message : String(e); } }); const toplamMs = Date.now() - t0; const kayitliProfil = (() => { const satir = profilOku.get(userId) as { alanlar: string } | undefined; try { return satir ? (JSON.parse(satir.alanlar) as Record) : {}; } catch { return {}; } })(); const hamMetin: string = tamMetin ?? yarimMetin ?? ""; const canliJevNotu = hamMetin.includes(JEV_DUZELTME_NOTU); // Sunucunun eklediği notlar modelin cevabı değildir: ayrılır, ayrıca kaydedilir const canliKayitNotu = hamMetin.includes(KAYIT_DUZELTME_NOTU); const metin = hamMetin.replace(JEV_DUZELTME_NOTU, "").replace(KAYIT_DUZELTME_NOTU, "").trim(); const m = metin.toLocaleLowerCase("tr-TR"); const cagrilan = araclar.map((a) => a.ad); const kontroller: Kontrol[] = []; // 0) akış if (tamMetin == null) kontroller.push({ ad: "akis", durum: "kaldi", neden: `cevap tamamlanmadı: ${akisHatasi ?? "?"}` }); // 1) araç kuralları if (s.beklenenAraclar.length) { const eksik = s.beklenenAraclar.filter((a) => !cagrilan.includes(a)); const ok = s.aracKurali === "herhangi" ? eksik.length < s.beklenenAraclar.length : eksik.length === 0; kontroller.push({ ad: "beklenen-arac", durum: ok ? "gecti" : "kaldi", neden: ok ? undefined : `çağrılmadı: ${eksik.join(", ")} (çağrılan: ${cagrilan.join(", ") || "yok"})` }); } const yasak = (s.yasakAraclar ?? []).filter((a) => cagrilan.includes(a)); if (s.yasakAraclar) kontroller.push({ ad: "yasak-arac", durum: yasak.length ? "kaldi" : "gecti", neden: yasak.length ? `çağrıldı: ${yasak.join(", ")}` : undefined }); const aracHatalari = araclar.filter((a) => a.hata); if (aracHatalari.length) kontroller.push({ ad: "arac-hatasi", durum: "gri", neden: aracHatalari.map((a) => `${a.ad}: ${kisalt(a.hata ?? "", 80)}`).join("; ") }); // 2) metin kontrolü kontroller.push(...icerikKontrolu(metin, s.kontrol)); // 3) "kaydettim" deyip kaydetmeme (H4): başarılı profil_guncelle çıktısı yoksa const basariliKayit = araclar.filter((a) => a.ad === "profil_guncelle" && a.cikti != null && typeof a.cikti === "object" && "kaydedildi" in (a.cikti as object)).length; if (/kaydettim|kaydedildi|profiline ekledim|profilini güncelledim/.test(m)) { kontroller.push({ ad: "kaydettim-ama-yazmadi", durum: basariliKayit ? "gecti" : "kaldi", neden: basariliKayit ? undefined : `cevap kayıt bildiriyor, başarılı profil_guncelle çağrısı yok (sunucu düzeltme notu: ${canliKayitNotu ? "eklendi" : "EKLENMEDİ"})` }); } // 4) vaat regex'i const vaat = vaatIfadeleri(metin); // 5) sayılar: araç çıktısı / liste / profil / soru / geçmiş / araç girdisi const kaynaklar = [system, s.soru, ...gecmis.map((g) => g.content), ...araclar.map((a) => JSON.stringify(a.girdi ?? "")), ...araclar.map((a) => JSON.stringify(a.cikti ?? ""))]; const sayi = metin ? dayanaksizSayilar(metin, kaynaklar, params.sira) : []; if (sayi.length) kontroller.push({ ad: "sayi", durum: "gri", neden: `kaynakta yok: ${sayi.join(", ")}` }); // 5b) veri yılı: "2024 verisi" gibi, ne SON_YIL ne de araç çıktısında / soruda geçen bir yıl const yanlisYil = [...new Set([...metin.matchAll(/(? e[1]))].filter( (y) => Number(y) !== SON_YIL && !kaynaklar.slice(1).some((k) => k.includes(y)), ); if (yanlisYil.length) kontroller.push({ ad: "veri-yili", durum: "kaldi", neden: `veri yılı ${SON_YIL}; cevapta: ${yanlisYil.join(", ")}` }); // 6) Jev danışman grubu (tek çağrı). Kullanıcı mesajı GİTMEZ. let jev: JevSonuc | null = null; if (metin) { const il = p.danismanProfili?.evIl ?? null; const jevSorulari: JevSoruAdi[] = JEV_SORU_GRUPLARI.danisman.filter((q) => q !== "profil_il" || il != null); let kalan = 24_000; const aracCiktisi = araclar .filter((a) => a.cikti != null) .map((a) => { const j = kisalt(JSON.stringify(a.cikti), Math.min(8000, Math.max(0, kalan))); kalan -= j.length; return { arac: a.ad, girdi: a.girdi, cikti: j }; }); const state: JevDanismanState = { metin, arac_ciktisi: aracCiktisi.length ? aracCiktisi : "yok", liste: listeOzeti.split("\n"), // Prompt'taki öteki meşru dayanaklar: kilitli kullanıcıya paket bilgisi, baraj tablosu baglam: [...(kilitli ? [KILITLI_URUN_BILGISI] : []), ...(params.tur === "tyt" ? [] : [barajBaglami])], profil: { sira: params.sira, puan_turu: PUAN_TURLERI[params.tur], il }, }; jev = await jevSor(state, jevSorulari); const kriz = s.etiketler.includes("kriz") ? "kriz" : s.etiketler.includes("kriz-degil") ? "kriz-degil" : null; kontroller.push(...jevKontrolleri(jev, kriz, JEV_DANISMAN_ESIKLERI)); } const jevVaatP = jev?.durum === "ok" ? (jev.p.yerlesme_vaadi ?? 0) : 0; // Hakem regex'i tek başına "gri"; Jev inceleme eşiğini geçtiyse ya da üretimdeki regex de yakaladıysa "kaldı" const uretimVaat = vaatDiliBul(metin); if (vaat.length || uretimVaat.length) { const kesin = uretimVaat.length > 0 || jevVaatP >= (JEV_DANISMAN_ESIKLERI.yerlesme_vaadi?.inceleme ?? 0.35); kontroller.push({ ad: "vaat-regex", durum: kesin ? "kaldi" : "gri", neden: (uretimVaat.length ? uretimVaat : vaat).join(" | ") }); } const karar: Karar = genelKarar(kontroller); const sorunlar = kontroller.filter((k) => k.durum !== "gecti"); asilInfo(` ${karar.padEnd(6)} ${s.id}${tekrar > 1 ? `#${tekrarNo + 1}` : ""} [${s.etiketler.join(",")}] ${(toplamMs / 1000).toFixed(1)} sn, ${adim} adım, araç: ${cagrilan.join("+") || "—"}${sorunlar.length ? ` ⇒ ${sorunlar.map((k) => `${k.ad}${k.neden ? `(${kisalt(k.neden, 90)})` : ""}`).join("; ")}` : ""}`); return { id: s.id, tekrarNo, profilId: s.profilId, etiketler: s.etiketler, soru: s.soru, gecmisVar: gecmis.length > 0, beklenenAraclar: s.beklenenAraclar, beklenenDavranis: s.beklenenDavranis, listeKaynagi: listeKaynagi.get(p.id), karar, kontroller: sorunlar, araclar: araclar.map((a) => ({ ad: a.ad, girdi: a.girdi, hata: a.hata, cikti: a.cikti == null ? null : kisalt(JSON.stringify(a.cikti), 1500) })), adim, cevap: metin, canliJevNotu, canliKayitNotu, canliDenetim: canliDenetim as CanliDenetim | null, kayitliProfil, jev: jev?.durum === "ok" ? jev.p : jev ? { atlandi: jev.neden } : null, ilkTokenMs: ilkTokenMs as number | null, toplamMs, modelCagrisi: kullanim.cagri, girdiToken: kullanim.girdiToken, ciktiToken: kullanim.ciktiToken, maliyet: kullanim.maliyet, }; } const isler = sorular.flatMap((s) => Array.from({ length: tekrar }, (_, tekrarNo) => ({ s, tekrarNo }))); const sonuclar = (await havuzdaKos(isler, eszaman, soruKos, () => TOPLAM.maliyet >= butce)).filter((x) => x != null); console.info = asilInfo; // ── özet ── const oran = (d: typeof sonuclar) => { const g = d.filter((x) => x.karar === "gecti").length; const gri = d.filter((x) => x.karar === "gri").length; const k = d.filter((x) => x.karar === "kaldi").length; return { n: d.length, gecti: g, gri, kaldi: k, gecmeOrani: d.length ? Math.round((100 * g) / d.length) : 0 }; }; const grupla = (anahtarlar: (x: (typeof sonuclar)[number]) => string[]) => { const g = new Map(); for (const x of sonuclar) for (const a of anahtarlar(x)) g.set(a, [...(g.get(a) ?? []), x]); return Object.fromEntries([...g].sort((a, b) => b[1].length - a[1].length).map(([a, d]) => [a, oran(d)])); }; const etiketBasina = grupla((x) => x.etiketler); const aracBasina = grupla((x) => x.beklenenAraclar); const kontrolBasina: Record = {}; for (const x of sonuclar) for (const k of x.kontroller) { const o = (kontrolBasina[k.ad] ??= { kaldi: 0, gri: 0, atlandi: 0 }); if (k.durum !== "gecti") o[k.durum]++; } // Tekrar tutarlılığı: soru başına karar dağılımı (model stokastik) const soruBasina = new Map(); for (const x of sonuclar) { const o = soruBasina.get(x.id) ?? { etiketler: x.etiketler, gecti: 0, gri: 0, kaldi: 0 }; if (x.karar === "gecti" || x.karar === "gri" || x.karar === "kaldi") o[x.karar]++; soruBasina.set(x.id, o); } const tutarlilik = tekrar > 1 ? { tekrar, hepGecti: [...soruBasina].filter(([, o]) => o.gri + o.kaldi === 0).map(([id]) => id), hicGecmedi: [...soruBasina].filter(([, o]) => o.gecti === 0).map(([id]) => id), karisik: Object.fromEntries([...soruBasina].filter(([, o]) => o.gecti > 0 && o.gri + o.kaldi > 0).map(([id, o]) => [id, `${o.gecti} geçti / ${o.gri} gri / ${o.kaldi} kaldı`])), } : null; const sure = sonuclar.map((x) => x.toplamMs); const ilk = sonuclar.map((x) => x.ilkTokenMs).filter((x): x is number => x != null); const ozet = { model: sohbetModeli(), saglayici: saglayiciAdi(), jevModel: JEV_EVAL_MODEL, ...oran(sonuclar), atlananSoru: isler.length - sonuclar.length, canliJevNotuEklenen: sonuclar.filter((x) => x.canliJevNotu).length, canliKayitNotuEklenen: sonuclar.filter((x) => x.canliKayitNotu).length, aracSiniriReddi: sonuclar.reduce((a, x) => a + (x.canliDenetim?.reddedilenArac ?? 0), 0), ortalamaAdim: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.adim, 0) / sonuclar.length).toFixed(2) : 0, ortalamaAracCagrisi: sonuclar.length ? +(sonuclar.reduce((a, x) => a + x.araclar.length, 0) / sonuclar.length).toFixed(2) : 0, enCokAracCagrisi: Math.max(0, ...sonuclar.map((x) => x.araclar.length)), soruBasiMaliyet: sonuclar.length ? +(TOPLAM.maliyet / sonuclar.length).toFixed(5) : 0, tutarlilik, gecikmeMs: { ilkToken: { p50: yuzdelik(ilk, 50), p95: yuzdelik(ilk, 95) }, toplam: { p50: yuzdelik(sure, 50), p95: yuzdelik(sure, 95), max: Math.max(0, ...sure) } }, harcama: { ...TOPLAM, jev: { ...JEV_TOPLAM, maliyet: jevMaliyeti() } }, etiketBasina, aracBasina, kontrolBasina, }; const satir = (o: Record>) => Object.entries(o).map(([a, v]) => [a, v.n, v.gecti, v.gri, v.kaldi, `%${v.gecmeOrani}`]); console.log(`\nÖzet: ${ozet.gecti} geçti · ${ozet.gri} gri · ${ozet.kaldi} kaldı / ${ozet.n} (%${ozet.gecmeOrani})${ozet.atlananSoru ? ` — bütçe freni: ${ozet.atlananSoru} soru koşulmadı` : ""}`); console.log(`\n${tablo(["etiket", "n", "geçti", "gri", "kaldı", "geçme"], satir(etiketBasina))}`); console.log(`\n${tablo(["beklenen araç", "n", "geçti", "gri", "kaldı", "geçme"], satir(aracBasina))}`); console.log(`\n${tablo(["kontrol", "kaldı", "gri", "atlandı"], Object.entries(kontrolBasina).sort((a, b) => b[1].kaldi - a[1].kaldi).map(([a, v]) => [a, v.kaldi, v.gri, v.atlandi]))}`); if (tutarlilik) { console.log(`\nTutarlılık (${tekrar} tekrar): ${tutarlilik.hepGecti.length} soru hep geçti · ${Object.keys(tutarlilik.karisik).length} karışık · ${tutarlilik.hicGecmedi.length} hiç geçmedi`); for (const [id, o] of Object.entries(tutarlilik.karisik)) console.log(` karışık ${id}: ${o}`); if (tutarlilik.hicGecmedi.length) console.log(` hiç geçmedi: ${tutarlilik.hicGecmedi.join(", ")}`); } console.log(`\nGecikme: ilk token p50 ${ozet.gecikmeMs.ilkToken.p50} ms / p95 ${ozet.gecikmeMs.ilkToken.p95} ms · toplam p50 ${ozet.gecikmeMs.toplam.p50} ms / p95 ${ozet.gecikmeMs.toplam.p95} ms · ortalama ${ozet.ortalamaAdim} adım, ${ozet.ortalamaAracCagrisi} araç çağrısı (en çok ${ozet.enCokAracCagrisi})`); console.log(`Harcama: ${TOPLAM.cagri} model çağrısı, ${TOPLAM.girdiToken} girdi + ${TOPLAM.ciktiToken} çıktı token, ${TOPLAM.maliyet.toFixed(4)} $${TOPLAM.maliyetBilinmeyenCagri ? ` (+${TOPLAM.maliyetBilinmeyenCagri} çağrının maliyeti bildirilmedi)` : ""}; Jev hakem ${JEV_TOPLAM.cagri} çağrı ≈ ${jevMaliyeti().toFixed(4)} $ (${JEV_TOPLAM.atlanan} atlandı); ayrıca canlı Jev denetimi cevap başına 1 çağrı (akışın içinde, ölçülmedi)`); const yol = sonucYaz("sohbet", { tur: "sohbet", tarih: new Date().toISOString(), git: gitDurumu(), argv: process.argv.slice(2), listeDosyasi: robotYolu?.split("/").pop() ?? null, ozet, sorular: sonuclar }); console.log(`\nHam sonuç: ${yol}`); sqlite.close(); process.exit(0); } main().catch((e) => { console.error(e); process.exit(2); });