"""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). Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asmamali. """ import json import os import re import threading import time import urllib.request TAVAN = 272_000 # abonelik modellerinin penceresi; ozetleyen model bundan fazlasini okuyamaz 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"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/"):] 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: 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"} sonuc["pencere"] = max(TABAN, min(TAVAN, 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