Second batch from the ten read-only audits; 126 bridge tests. Among them: a spoken number
("opus beş beş") resolves, a used route no longer expires after 7 days, the local reply says how to
go back, an unrouted 9r/ name gets a local 503 instead of going to chatgpt.com, a combo request is not
resent after the upstream closed it, the last model list is served when chatgpt.com is unreachable,
trimming keeps the real last user message, Claude-member combos use the Claude estimate, CRLF SSE is
handled, log lines carry the date and the status endpoint reports a code fingerprint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
369 lines
16 KiB
Python
369 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).
|
||
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
|