Measured with the unmodified Codex binary on 10 Oct 2026: - real context windows per model (NanoGPT list, local table, combo = its smallest member, capped at the summarizer's window) so Codex compacts by itself when switching to a smaller model; an overflow guard trims oversized old parts and reports what still does not fit - classic tools instead of code mode for external models: GLM 0/3 -> 3/3, Qwen 0/3 -> 2/3 on simple tool tasks, Claude/cx/Gemini unchanged - phase (commentary / final_answer) on external assistant messages - an answer cut by the client is carried to the next turn once - images stay for vision-capable targets - summary brake stops tight loops, not frequent compaction - permanent gateway errors and tool-less targets fail at once with a readable message instead of five reconnects - a lone "." (used to apply a model change in voice) is answered locally - the bridge listens within seconds; the IDE waits 45 s for it Release: AITURK IDE 1.0.0-beta.26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
293 lines
12 KiB
Python
293 lines
12 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 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 = yol + ".tmp"
|
||
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 {}
|
||
if onbellek.get("modeller") and simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN:
|
||
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 tasma_mi(metin):
|
||
return bool(_TASMA.search(metin or ""))
|
||
|
||
|
||
def sinir_oku(metin):
|
||
"""Hedefin tasma hatasindaki gercek sinir; yoksa 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 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
|
||
|
||
|
||
def tahmini_token(req):
|
||
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / BAYT_BOLU_TOKEN)
|
||
|
||
|
||
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 []
|
||
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"), 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):
|
||
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
|
||
sinir = int(pencere_token * DOLULUK)
|
||
tahmin = tahmini_token(req)
|
||
if not pencere_token or tahmin <= sinir:
|
||
return 0, True, tahmin
|
||
kirpilan = 0
|
||
parcalar = sorted(_metin_parcalari(req), key=lambda sa: len(sa[0][sa[1]]), reverse=True)
|
||
for sahip, anahtar in parcalar:
|
||
metin = sahip[anahtar]
|
||
if len(metin) < KIRP_ESIK:
|
||
break
|
||
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)
|
||
if tahmin <= sinir:
|
||
return kirpilan, True, tahmin
|
||
return kirpilan, tahmin <= sinir, tahmin
|