Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/pencere.py
T
yilsemandClaude Opus 5.5 7c0dc467d5 fix(codex-bridge): remaining audit findings (routing, trimming, retries, model list fallback, logging)
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>
2026-10-11 01:50:08 +03:00

369 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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