feat(codex-link): bridge 7 - external models work like subscription models

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>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-10 22:04:58 +03:00
1 parent ac0fdcf355
commit acf88f8314
19 files changed
+1160 -47

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
"""Yarida kesilen cevabi sonraki modele tasir (10 Ekim 2026).
Olcum: Codex kapanir ya da kullanici cevabi durdurursa o ana kadar yazilan metin sohbet gecmisine girmiyor;
sonraki tur (ayni ya da baska modelle) "onceki cevabi goremiyorum" diyor. Kopru akisi zaten goruyor:
ISTEMCI baglantiyi kapattiginda (yanit bitmeden) o ana kadarki metni bellekte tutar ve ayni sohbetin
sonraki isteginde, kullanicinin yeni mesajina kisa bir not olarak ekler. Bir kez kullanilir.
Bilerek yapilmayanlar:
- Ust kaynak kopmasinda (istemci hala bagli) kayit yok: Codex o istegi kendisi yeniden gonderir; not eklenirse
model bastan yazmak yerine ortadan devam eder ve cevabin basi kaybolur.
- Yarim metin asistan mesaji olarak eklenmez: Claude "asistan mesajiyla biten istek"i reddeder ve abonelik
yolunda kimliksiz asistan ogesi risklidir. Kullanici mesajina eklenen not her saglayicida gecerlidir.
- Diske yazilmaz; kopru yeniden baslarsa kayit gider.
"""
import json
import threading
import time
OMUR_SN = 6 * 3600
EN_COK_KAYIT = 50
EN_COK_KARAKTER = 12_000 # nota konan yarim metnin ust siniri (basi ve sonu korunur)
EN_AZ_KARAKTER = 40 # bundan kisa yarim metin tasinmaya degmez
NOT_BASI = "[Köprü notu: bir önceki cevabın yarıda kesildi ve sohbet geçmişine kaydedilmedi."
_KILIT = threading.Lock()
_KAYIT = {} # sohbet anahtari -> {"zaman", "metin", "model"}
class Izleyici:
"""Akistan gecen parcalardan asistan metnini toplar; yanitin bitip bitmedigini izler."""
def __init__(self, anahtar, model):
self.anahtar, self.model = anahtar, model
self.buf = b""
self.parcalar = []
self.bitti = False
def feed(self, chunk):
self.buf += chunk
while b"\n\n" in self.buf:
blok, self.buf = self.buf.split(b"\n\n", 1)
if b"response.output_text.delta" in blok:
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
d = json.loads(satir[5:])
except ValueError:
continue
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
self.parcalar.append(d["delta"])
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
self.bitti = True
@property
def metin(self):
return "".join(self.parcalar)
def kaydet(izleyici, simdi=None):
"""Istemci kapatti ve yanit bitmemisti: yarim metni sakla. True: saklandi."""
metin = izleyici.metin.strip()
if not izleyici.anahtar or izleyici.bitti or len(metin) < EN_AZ_KARAKTER:
return False
simdi = time.time() if simdi is None else simdi
with _KILIT:
_KAYIT[izleyici.anahtar] = {"zaman": simdi, "metin": metin, "model": izleyici.model}
for eski in sorted(_KAYIT, key=lambda k: _KAYIT[k]["zaman"])[:max(0, len(_KAYIT) - EN_COK_KAYIT)]:
del _KAYIT[eski]
return True
def _kisalt(metin):
if len(metin) <= EN_COK_KARAKTER:
return metin
bas, son = EN_COK_KARAKTER // 4, EN_COK_KARAKTER - EN_COK_KARAKTER // 4
return metin[:bas] + f"\n[... ortadan {len(metin) - bas - son} karakter çıkarıldı ...]\n" + metin[-son:]
def uygula(req, anahtar, simdi=None):
"""Ayni sohbetin sonraki isteginde yarim metni kullanicinin son mesajina not olarak ekler.
Donus: eklenen karakter sayisi (0: eklenmedi). Kayit yalniz basariyla eklenince ya da eskiyince silinir."""
if not anahtar:
return 0
simdi = time.time() if simdi is None else simdi
with _KILIT:
kayit = _KAYIT.get(anahtar)
if kayit and simdi - kayit["zaman"] > OMUR_SN:
del _KAYIT[anahtar]
kayit = None
girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi:
return 0
son = girdi[-1]
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"]
ham = json.dumps(girdi, ensure_ascii=False)
if json.dumps(metin[:200], ensure_ascii=False)[1:-1] in ham:
with _KILIT: # Codex bu metni gecmise kendisi yazmis: ikinci kez eklenmez
_KAYIT.pop(anahtar, None)
return 0
notu = (f"{NOT_BASI} Kesilmeden önce yazdığın metin aşağıda. Kullanıcı devam etmeni isterse baştan yazma, "
f"kaldığın yerden sürdür.]\n<yarim_cevap model=\"{kayit['model']}\">\n{_kisalt(metin)}\n</yarim_cevap>")
icerik = son.get("content")
if isinstance(icerik, str):
son["content"] = [{"type": "input_text", "text": icerik}, {"type": "input_text", "text": notu}]
elif isinstance(icerik, list):
icerik.append({"type": "input_text", "text": notu})
else:
return 0
with _KILIT:
_KAYIT.pop(anahtar, None)
return len(notu)