Separate encrypted reasoning from actual compaction, stop migration after native return, and retain full history and recent exchanges. Add typed settings, Gemini fallback, verified provider windows, bounded summaries and durable concurrent checkpoints. Validation: 245 bridge tests plus focused concurrency tests; 71 settings/connection tests; 66 supplementary tests; TypeScript checks; renderer and Electron bundles; real primary/native/fallback summary probes and 700K input acceptance.
379 lines
16 KiB
Python
379 lines
16 KiB
Python
"""Eklenen modellerin gercek baglam penceresi ve yetenekleri (10 Ekim 2026).
|
||
|
||
Neden: gecit model basina pencere vermiyor; kopru her modele abonelik kalibinin penceresini (272000)
|
||
kopyaliyordu. Codex, kucuk pencereli modele geciste sohbeti kendisi sikistirir, ama bunu ilan edilen
|
||
pencereye gore yapar (core/src/session/turn.rs maybe_run_previous_model_inline_compact). Pencere dogru
|
||
ilan edilince o yol kendiliginden calisir; yanlis ilan edilince istek hedefe sigmaz.
|
||
|
||
Kaynak sirasi (kucukten guvenilire degil, guvenilirden yedege):
|
||
1. Hedefin kendi hatasindan ogrenilen sinir (ogren / ogrenilen): en guvenilir, diske yazilir.
|
||
2. NanoGPT'nin acik model listesi (nano/... modelleri icin istegi tasiyan saglayicinin kendi degeri).
|
||
3. Saglayici kisaltmasina gore yerel tablo (cc, cx, ag).
|
||
4. Bilinmeyen: VARSAYILAN (temkinli) ve bir kez gunluk satiri.
|
||
Combo: yapraklarinin en kucugu (yedege dusulurse de sigsin).
|
||
Sikistirici ve hedef modelin pencereleri ayri hesaplanir; genel abonelik tavani uygulanmaz.
|
||
"""
|
||
import json
|
||
import os
|
||
import re
|
||
import threading
|
||
import time
|
||
import urllib.request
|
||
|
||
VARSAYILAN = 128_000 # hakkinda hicbir sey bilinmeyen model
|
||
TABAN = 8_000 # saglayici verisi bozuksa (0, eksi) kullanilmaz
|
||
NANO_URL = "https://nano-gpt.com/api/v1/models?detailed=true"
|
||
NANO_OMUR_SN = 24 * 3600
|
||
NANO_DOSYA = "pencere-nano.json"
|
||
OGRENILEN_DOSYA = "pencere-ogrenilen.json"
|
||
ARACSIZ_DOSYA = "aracsiz-modeller.json"
|
||
# Saglayici kisaltmasi -> (pencere, gorsel girdi, arac cagrisi). cc = Claude Code aboneligi, cx = Codex
|
||
# aboneligi, ag = Antigravity. Degerler abonelik yollarinin temkinli sinirlaridir.
|
||
_YEREL = (
|
||
(re.compile(r"^cx/"), (272_000, True, True)),
|
||
(re.compile(r"^cc/"), (200_000, True, True)),
|
||
(re.compile(r"^ag/claude"), (200_000, True, True)),
|
||
(re.compile(r"^ag/gemini"), (1_000_000, True, True)),
|
||
(re.compile(r"^ag/gpt-oss"), (131_072, False, True)),
|
||
)
|
||
# "maximum context length is 131072 tokens", "prompt is too long: 3040251 tokens > 1000000 maximum",
|
||
# "context window of 32768 tokens", "exceeds the limit of 65536" ...
|
||
_SINIR = (
|
||
re.compile(r"max(?:imum)? input(?: tokens)?\s*[:=]?\s*(\d{4,8})", re.I),
|
||
re.compile(r"maximum context length (?:is|of) (\d{4,8})", re.I),
|
||
re.compile(r"\d{4,9} tokens? > (\d{4,8}) maximum", re.I),
|
||
re.compile(r"context (?:window|length|size)(?: limit)? (?:is|of) (\d{4,8})", re.I),
|
||
re.compile(r"(?:limit|maximum) of (\d{4,8}) tokens", re.I),
|
||
re.compile(r"max(?:imum)? context tokens\D{0,6}(\d{4,8})", re.I),
|
||
)
|
||
_TASMA = re.compile(r"context[_ ](?:length|window)[_ ]exceeded|maximum context length|prompt is too long|"
|
||
r"context window|too many tokens|input is too long|exceeds? the (?:maximum |model.s )?context|max(?:imum)? context tokens", re.I)
|
||
|
||
|
||
def _oku(yol):
|
||
try:
|
||
with open(yol, encoding="utf-8") as f:
|
||
v = json.load(f)
|
||
return v if isinstance(v, dict) else {}
|
||
except (OSError, ValueError):
|
||
return {}
|
||
|
||
|
||
def _yaz(yol, veri):
|
||
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # es zamanli iki yazma ayni gecici dosyada carpismasin
|
||
with open(gecici, "w", encoding="utf-8") as f:
|
||
json.dump(veri, f, ensure_ascii=False)
|
||
os.replace(gecici, yol)
|
||
|
||
|
||
def nano_sadelestir(ham):
|
||
"""NanoGPT yaniti -> {model kimligi (kucuk harf): {"pencere", "gorsel", "arac"}}."""
|
||
cikti = {}
|
||
for m in (ham or {}).get("data") or []:
|
||
kimlik = str(m.get("id") or "").lower()
|
||
pencere = m.get("context_length")
|
||
if not kimlik or not isinstance(pencere, int) or pencere < TABAN:
|
||
continue
|
||
yetenek = m.get("capabilities") or {}
|
||
cikti[kimlik] = {"pencere": pencere, "gorsel": bool(yetenek.get("vision")),
|
||
"arac": bool(yetenek.get("tool_calling"))}
|
||
return cikti
|
||
|
||
|
||
def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir=None, ag=True):
|
||
"""Onbellek tazeyse onu, degilse agdan yenisini dondurur. Ag hatasinda eski onbellek (yasina bakilmadan)
|
||
kullanilir; o da yoksa bos sozluk doner ve cagiran yerel tabloya duser. Hicbir durumda istisna atmaz.
|
||
ag=False: aga cikilmaz, eldeki onbellek doner (koprunun acilisi beklemesin diye)."""
|
||
simdi = time.time() if simdi is None else simdi
|
||
yol = os.path.join(home, NANO_DOSYA) if home else ""
|
||
onbellek = _oku(yol) if yol else {}
|
||
try:
|
||
taze = simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN
|
||
except (TypeError, ValueError):
|
||
taze = False # bozuk "zaman" alani yenilemeyi engellemez
|
||
if onbellek.get("modeller") and taze:
|
||
return onbellek["modeller"]
|
||
if not ag:
|
||
return onbellek.get("modeller") or {}
|
||
try:
|
||
if getir is None:
|
||
istek = urllib.request.Request(NANO_URL, headers={"User-Agent": ua, "Accept": "application/json"})
|
||
with urllib.request.urlopen(istek, timeout=timeout) as resp:
|
||
ham = json.load(resp)
|
||
else:
|
||
ham = getir()
|
||
modeller = nano_sadelestir(ham)
|
||
if modeller:
|
||
if yol:
|
||
try:
|
||
_yaz(yol, {"zaman": simdi, "modeller": modeller})
|
||
except OSError:
|
||
pass
|
||
return modeller
|
||
except Exception: # noqa: BLE001 - ag, JSON, sertifika: hepsi "yerel tabloya dus" demek
|
||
pass
|
||
return onbellek.get("modeller") or {}
|
||
|
||
|
||
def ogrenilen(home):
|
||
return {k: int(v) for k, v in _oku(os.path.join(home, OGRENILEN_DOSYA)).items()
|
||
if isinstance(v, (int, float)) and v >= TABAN} if home else {}
|
||
|
||
|
||
def aracsizlar(home):
|
||
"""Hedefin "arac cagrisi desteklenmiyor" dedigi modeller (kimlik kumesi)."""
|
||
return {k for k, v in _oku(os.path.join(home, ARACSIZ_DOSYA)).items() if v} if home else set()
|
||
|
||
|
||
def aracsiz_mi(metin):
|
||
return bool(re.search(r"does not support tool|tool_choice_unsupported|tools? (?:are|is) not supported|"
|
||
r"function calling is not supported", metin or "", re.I))
|
||
|
||
|
||
def ogren_aracsiz(home, kimlik):
|
||
"""True: kayit yeni eklendi."""
|
||
if not home or not kimlik:
|
||
return False
|
||
yol = os.path.join(home, ARACSIZ_DOSYA)
|
||
veri = _oku(yol)
|
||
if veri.get(kimlik):
|
||
return False
|
||
veri[kimlik] = True
|
||
try:
|
||
_yaz(yol, veri)
|
||
except OSError:
|
||
return False
|
||
return True
|
||
|
||
|
||
def kapali_hata_mi(metin):
|
||
"""Gecit asil nedeni gizleyip yalniz "upstream 400" derse (olcum 11 Ekim 2026)."""
|
||
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
|
||
|
||
|
||
_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
|
||
|
||
|
||
def hiz_siniri_mi(metin):
|
||
"""Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
|
||
return bool(_HIZ_SINIRI.search(metin or ""))
|
||
|
||
|
||
def tasma_mi(metin):
|
||
return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
|
||
|
||
|
||
def sinir_oku(metin):
|
||
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
|
||
if hiz_siniri_mi(metin):
|
||
return None
|
||
for kalip in _SINIR:
|
||
m = kalip.search(metin or "")
|
||
if m and int(m.group(1)) >= TABAN:
|
||
return int(m.group(1))
|
||
return None
|
||
|
||
|
||
def ogren(home, kimlik, sinir):
|
||
"""Hedefin bildirdigi siniri kaydeder (yalniz kucultur). True: kayit degisti."""
|
||
if not home or not kimlik or not sinir or sinir < TABAN:
|
||
return False
|
||
yol = os.path.join(home, OGRENILEN_DOSYA)
|
||
veri = _oku(yol)
|
||
if isinstance(veri.get(kimlik), (int, float)) and veri[kimlik] <= sinir:
|
||
return False
|
||
veri[kimlik] = int(sinir)
|
||
try:
|
||
_yaz(yol, veri)
|
||
except OSError:
|
||
return False
|
||
return True
|
||
|
||
|
||
def yol_bilgisi(yol, nano):
|
||
""""nano/z-ai/glm-5.3" ya da "cc/claude-opus-5-5" -> {"pencere","gorsel","arac","kaynak"} ya da None."""
|
||
yol = (yol or "").strip()
|
||
if yol.startswith("9router/"):
|
||
yol = yol[len("9router/"):]
|
||
for current, legacy in (("agy/", "ag/"), ("antigravity/", "ag/"), ("codex/", "cx/")):
|
||
if yol.startswith(current):
|
||
yol = legacy + yol[len(current):]
|
||
break
|
||
if yol.startswith("nano/"):
|
||
n = (nano or {}).get(yol[len("nano/"):].lower())
|
||
if n:
|
||
return {**n, "kaynak": "nanogpt"}
|
||
return None
|
||
for kalip, (pencere, gorsel, arac) in _YEREL:
|
||
if kalip.match(yol):
|
||
return {"pencere": pencere, "gorsel": gorsel, "arac": arac, "kaynak": "yerel"}
|
||
return None
|
||
|
||
|
||
def _yapraklar(dugum, derinlik=0):
|
||
if not isinstance(dugum, dict) or derinlik > 6:
|
||
return []
|
||
if dugum.get("kind") != "combo":
|
||
return [dugum.get("model")] if dugum.get("model") else []
|
||
cikti = []
|
||
for uye in dugum.get("members") or []:
|
||
cikti.extend(_yapraklar(uye, derinlik + 1))
|
||
return cikti
|
||
|
||
|
||
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
|
||
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
|
||
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
|
||
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
|
||
bilinmeyen = {"pencere": VARSAYILAN, "gorsel": False, "arac": True, "kaynak": "varsayilan"}
|
||
if tur == "combo":
|
||
yapraklar = _yapraklar((kayit or {}).get("topology"))
|
||
bilgiler = [yol_bilgisi(y, nano) or dict(bilinmeyen) for y in yapraklar]
|
||
if not bilgiler:
|
||
sonuc = dict(bilinmeyen)
|
||
else:
|
||
sonuc = {"pencere": min(b["pencere"] for b in bilgiler),
|
||
# Bir takim uyesi resmi goremese de digerleri gorur; sirali yedekte ilk uye belirler.
|
||
"gorsel": bool(bilgiler[0]["gorsel"]), "arac": bool(bilgiler[0]["arac"]),
|
||
"kaynak": "uyeler:" + ",".join(sorted({b["kaynak"] for b in bilgiler}))}
|
||
else:
|
||
# Current gateway model descriptions are display names. Resolve only
|
||
# recognized provider-qualified IDs; never infer capacity from a name.
|
||
if kimlik and kimlik.startswith('agy-'):
|
||
yol = 'agy/' + kimlik[len('agy-'):]
|
||
if (yol or "").startswith("no-think/"):
|
||
# Dusunmesiz kopya: asil saglayici kimlikten okunur ("no-think-9router-cc-claude-..." -> cc).
|
||
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
|
||
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
|
||
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
|
||
if kimlik in aracsiz:
|
||
sonuc = {**sonuc, "arac": False}
|
||
sinir = (ogr or {}).get(kimlik)
|
||
if sinir and sinir < sonuc["pencere"]:
|
||
sonuc = {**sonuc, "pencere": int(sinir), "kaynak": "ogrenilen"}
|
||
# Summarizers now have their own provider-specific budgets. A subscription
|
||
# model's window must not cap unrelated providers' advertised capacity.
|
||
sonuc["pencere"] = max(TABAN, int(sonuc["pencere"]))
|
||
return sonuc
|
||
|
||
|
||
def claude_var(tur, yol, kayit=None):
|
||
"""Istek Claude'a gidebilir mi? Tek modelde yolundan, comboda yapraklarindan okunur (Opus-5 gibi combolarin
|
||
adinda "claude" gecmez; token tahmini yine Claude oraniyla yapilmalidir)."""
|
||
yollar = _yapraklar((kayit or {}).get("topology")) if tur == "combo" else [yol]
|
||
for y in yollar:
|
||
y = str(y or "").lower()
|
||
if y.startswith("9router/"):
|
||
y = y[len("9router/"):]
|
||
if "claude" in y or y.startswith("cc/"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def bekci_calisir(kaynak, pencere_token):
|
||
"""Tasma bekcisi bu pencereye guvenebilir mi? Hakkinda hicbir sey bilinmeyen modelde calismaz (tahmine
|
||
dayanarak gecmis kirpilmaz). Comboda tek bilinmeyen uye, bilinen KUCUK uyenin sinirini gecersiz kilmaz:
|
||
ilan edilen pencere VARSAYILAN'dan kucukse o deger bilinen bir uyeden gelmistir."""
|
||
kaynak = kaynak or "varsayilan"
|
||
if "varsayilan" not in kaynak:
|
||
return True
|
||
return kaynak.startswith("uyeler:") and kaynak != "uyeler:varsayilan" and int(pencere_token or 0) < VARSAYILAN
|
||
|
||
|
||
def kisa(pencere):
|
||
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
|
||
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
|
||
|
||
|
||
# ---- Istek oncesi tasma bekcisi ----
|
||
# Codex sikistirmadan sonra kullanici mesajlarini AYNEN tutar; cok buyuk bir yapistirma (ya da dev arac
|
||
# ciktisi) kucuk pencereli hedefe yine sigmaz ve hedef bos akisla doner (10 Ekim 2026 olcumu: 390 KB istek,
|
||
# 64K pencere, Codex 5 kez yeniden deneyip "stream closed" ile dustu). Bekci, hedefe giden KOPYADA en buyuk
|
||
# eski metin parcalarinin ortasini cikarir; yine sigmazsa istegi gondermeden anlasilir hata doner.
|
||
BAYT_BOLU_TOKEN = 3.2 # olcum (10 Ekim 2026): 540 KB duz Turkce metin = 171016 token (3,16 bayt/token)
|
||
DOLULUK = 0.97 # pencerenin bu oranini asan istek kirpilir / gonderilmez
|
||
KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
|
||
KIRP_BAS = KIRP_SON = 1500
|
||
|
||
|
||
# Claude ayni metni daha cok tokena boler. Olcum (11 Ekim 2026): 607 KB'lik gercek Codex istegi 3,2 oraniyla
|
||
# 189776 token tahmin edildi (pencere 200000) ve Claude tarafindan reddedildi; yani gercek oran 3,03'un altinda.
|
||
CLAUDE_BAYT_BOLU_TOKEN = 2.6
|
||
|
||
|
||
def oran_sec(hedef, claude=False):
|
||
"""claude=True: hedefin adinda gecmese de (combo) istek Claude'a gidebilir (bkz. claude_var)."""
|
||
return CLAUDE_BAYT_BOLU_TOKEN if (claude or "claude" in str(hedef or "").lower()) else BAYT_BOLU_TOKEN
|
||
|
||
|
||
def tahmini_token(req, oran=None):
|
||
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / (oran or BAYT_BOLU_TOKEN))
|
||
|
||
|
||
KOPRU_DEVAM_BASI = "(Önceki cevabın yarıda kaldı" # bridge.py claude_sonu_duzelt'in ekledigi mesajin basi
|
||
|
||
|
||
def _kopru_mesaji(oge):
|
||
icerik = oge.get("content")
|
||
if isinstance(icerik, list):
|
||
icerik = "".join(str(p.get("text") or "") for p in icerik if isinstance(p, dict))
|
||
return isinstance(icerik, str) and icerik.startswith(KOPRU_DEVAM_BASI)
|
||
|
||
|
||
def _son_arac_ciktisi(req):
|
||
"""En yeni arac ciktisinin metin sahipleri (kimlik kumesi): model bir sonraki adimi buna gore atar,
|
||
bu yuzden ancak baska hicbir sey yetmezse kirpilir."""
|
||
for it in reversed(req.get("input") or []):
|
||
if isinstance(it, dict) and "output" in it and str(it.get("type") or "").endswith("_output"):
|
||
deger = it.get("output")
|
||
return {id(it)} if isinstance(deger, str) else {id(p) for p in deger or [] if isinstance(p, dict)}
|
||
return set()
|
||
|
||
|
||
def _metin_parcalari(req):
|
||
"""Kirpilabilir (sahip, anahtar) ciftleri: arac ciktilari ile eski kullanici/asistan mesajlari.
|
||
Son kullanici mesajina ve talimat tasiyan developer/system mesajlarina (Codex kurallari, skill
|
||
yonergeleri, kanca baglami) dokunulmaz."""
|
||
girdi = req.get("input")
|
||
if not isinstance(girdi, list):
|
||
return []
|
||
# Koprunun sona ekledigi "devam et" mesaji kullanicinin son mesaji degildir; korunan, ondan onceki gercek mesajdir.
|
||
son_kullanici = max((i for i, it in enumerate(girdi) if isinstance(it, dict) and it.get("role") == "user"
|
||
and it.get("type", "message") == "message" and not _kopru_mesaji(it)), default=-1)
|
||
parcalar = []
|
||
for i, it in enumerate(girdi):
|
||
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
|
||
continue
|
||
for alan in ("content", "output"):
|
||
deger = it.get(alan)
|
||
if isinstance(deger, str):
|
||
parcalar.append((it, alan))
|
||
elif isinstance(deger, list):
|
||
for p in deger:
|
||
if isinstance(p, dict) and isinstance(p.get("text"), str):
|
||
parcalar.append((p, "text"))
|
||
return parcalar
|
||
|
||
|
||
def sigdir(req, pencere_token, oran=None):
|
||
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
|
||
sinir = int(pencere_token * DOLULUK)
|
||
tahmin = tahmini_token(req, oran)
|
||
if not pencere_token or tahmin <= sinir:
|
||
return 0, True, tahmin
|
||
kirpilan = 0
|
||
en_yeni = _son_arac_ciktisi(req)
|
||
# Buyukten kucuge; en yeni arac ciktisi en sona kalir (yalniz digerleri yetmezse kirpilir).
|
||
parcalar = sorted(_metin_parcalari(req), key=lambda sa: (id(sa[0]) in en_yeni, -len(sa[0][sa[1]])))
|
||
for sahip, anahtar in parcalar:
|
||
metin = sahip[anahtar]
|
||
if len(metin) < KIRP_ESIK:
|
||
continue
|
||
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
|
||
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
|
||
kirpilan += 1
|
||
tahmin = tahmini_token(req, oran)
|
||
if tahmin <= sinir:
|
||
return kirpilan, True, tahmin
|
||
return kirpilan, tahmin <= sinir, tahmin
|