fix(codex-bridge): close the gaps an independent review found
- One decision per real turn: a turn starts with a user message after the previous final answer; tool steps, messages typed mid-turn, a lone "." and compaction keep the combo. Works without prompt_cache_key too. - The virtual Otomatik name never reaches the gateway (stage off or error -> last choice or targets.zor); prefilter.target may not be it. - finishCheck: at most once per turn; a broken, empty or refused retry delivers the already approved first answer. - Layer window filter: only trusted windows, never the last layer. - Correction channel only on always-pass targets while checks are on; Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame passes unfiltered instead of breaking the stream. - Turn-start questions wait in the local queue no longer than their timeout; decision-turn memory under a lock with refreshed order. - Dify fetch refuses redirects (the app key never moves host); the rules file write retries a Windows sharing violation. - An unreadable settings file no longer breaks every request. - TypeScript accepts only https:// URLs the bridge accepts; finishCheck.timeoutSeconds (unused) removed. Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the turn, prefilter (incl. decision timeout) and finish-check paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7c4ef18df0
commit
a7ba300448
10 files changed
+353
-91
No files matched your search
@@ -245,6 +245,7 @@ def load_ide_catalog(ag=True):
|
||||
for h in set(oto["targets"].values())),
|
||||
"arac": True, "kaynak": "", "claude": False}
|
||||
model_map[slug] = (IDE_ROUTE, oto["model"], None)
|
||||
OTO_ADI[0] = oto["model"]
|
||||
n += 1
|
||||
else:
|
||||
ROUTES.pop(IDE_ROUTE, None)
|
||||
@@ -263,68 +264,115 @@ def _oto_ayari():
|
||||
return None
|
||||
return s["autoRoute"] if s["enabled"] and s["autoRoute"]["enabled"] else None
|
||||
|
||||
# Tur basi kararlari: sohbet -> (tur anahtari, gelen slug, secilen slug). Ayni turun arac adimlari ayni karari kullanir.
|
||||
# Tur basi kararlari: sohbet -> (tur anahtari, gelen slug, secilen slug). Ayni turun adimlari ayni karari kullanir.
|
||||
ON_KARAR = {}
|
||||
ON_KARAR_SINIR = 500
|
||||
BITIS_TURLARI = {} # finishCheck kullanilan turlar (turda bir kez)
|
||||
KARAR_KILIDI = threading.Lock()
|
||||
OTO_ADI = ["Otomatik"] # katalogdaki sanal modelin son adi (karar asamasi hata verirse disari cikmasin)
|
||||
|
||||
# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler);
|
||||
# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir.
|
||||
load_ide_catalog(ag=False)
|
||||
|
||||
def tur_bilgisi(req, sohbet):
|
||||
"""(sohbet koku, tur anahtari, tur basindaki istek, yeni tur mu). Tur, calisan bir turun icine dusmeyen
|
||||
kullanici mesajiyla baslar. Arac adimlari, tur ortasinda yazilan mesaj, tek "." ve ozet istegi ayni turdadir;
|
||||
yeni tur yalniz son oge yeni bir kullanici istegi oldugunda baslar."""
|
||||
girdi = req.get("input") or []
|
||||
kok = sohbet or "anon:" + hashlib.sha256(json.dumps(girdi[:1], ensure_ascii=False, sort_keys=True,
|
||||
default=str).encode("utf-8", "replace")).hexdigest()
|
||||
bas, istek = -1, ""
|
||||
for i in range(len(girdi) - 1, -1, -1):
|
||||
metin = jev_denetim.istek_metni(girdi[i])
|
||||
if not metin or metin == ".":
|
||||
continue
|
||||
if not istek:
|
||||
bas, istek = i, metin # tur basi bulunamazsa en son gercek istek
|
||||
if not model_gecis.tur_ortasi(req, i):
|
||||
bas, istek = i, metin
|
||||
break
|
||||
son = model_gecis.son_sira(girdi)
|
||||
yeni = son >= 0 and son == bas and not model_gecis.tur_ortasi(req, son)
|
||||
tur = hashlib.sha256(f"{kok}\0{bas}\0{istek}".encode("utf-8", "replace")).hexdigest()
|
||||
return kok, tur, istek, yeni
|
||||
|
||||
|
||||
def karar_asamasi(n, req, sohbet, slug):
|
||||
"""14 Ekim 2026 karar asamalari: "Otomatik" modelin combo secimi (autoRoute) ve normal modelin reddedecegi
|
||||
tahmin edilen isi dogrudan ayardaki katmana verme (prefilter). Karar tur basinda bir kez verilir; ayni turun
|
||||
arac adimlari ayni karari kullanir (her adimda model degisirse saglayicilar arasi gecmis bozulur). Kapali ayar,
|
||||
bos istek ya da karar modeli hatasi bugunku yolu degistirmez (Otomatik icin guclu hedef secilir)."""
|
||||
tahmin edilen isi dogrudan ayardaki katmana verme (prefilter). Karar yalniz yeni turda verilir; ayni turun
|
||||
adimlari ayni karari kullanir (tur ortasinda model degisirse saglayicilar arasi gecmis bozulur). Kapali ayar,
|
||||
bos istek ya da karar modeli hatasi bugunku yolu degistirmez. Sanal model adi hicbir yolda gecide gitmez."""
|
||||
if not slug.startswith(IDE_PREFIX):
|
||||
return slug
|
||||
settings = jev_settings.read()[0]
|
||||
a, p = settings["autoRoute"], settings["prefilter"]
|
||||
sanal = slug == IDE_PREFIX + a["model"]
|
||||
kok, tur, istek, yeni = tur_bilgisi(req, sohbet)
|
||||
with KARAR_KILIDI:
|
||||
kayit = ON_KARAR.get(kok)
|
||||
if sanal and not (settings["enabled"] and a["enabled"]):
|
||||
# Kapatilmis Otomatik: son karar ya da guclu hedef; sanal ad gecide gitmez.
|
||||
secilen = kayit[2] if kayit and kayit[1] == slug else IDE_PREFIX + a["targets"]["zor"]
|
||||
log(f"[{n} {slug}] Otomatik kapali; bu adim {secilen} modeline gidiyor")
|
||||
req["model"] = secilen
|
||||
return secilen
|
||||
if not settings["enabled"]:
|
||||
return slug
|
||||
a, p = settings["autoRoute"], settings["prefilter"]
|
||||
oto = a["enabled"] and slug == IDE_PREFIX + a["model"]
|
||||
hedef = slug[len(IDE_PREFIX):]
|
||||
ret = p["enabled"] and (oto or not (jev_denetim.muaf_mi(hedef) or hedef == p["target"]
|
||||
or hedef in jev_denetim.sonrakiler(p["target"])))
|
||||
if not oto and not ret:
|
||||
ret = p["enabled"] and (sanal or not (jev_denetim.muaf_mi(hedef) or hedef == p["target"]
|
||||
or hedef in jev_denetim.sonrakiler(p["target"])))
|
||||
if not sanal and not ret:
|
||||
return slug
|
||||
istek, sira = jev_denetim.son_istek_sirasi(req)
|
||||
# Tur = son kullanici mesaji (metni ve girdideki yeri); ayni turun arac adimlarinda degismez.
|
||||
tur = hashlib.sha256(f"{sohbet}\0{sira}\0{istek}".encode("utf-8", "replace")).hexdigest()
|
||||
kayit = ON_KARAR.get(sohbet) if sohbet else None
|
||||
if kayit and kayit[0] == tur and kayit[1] == slug:
|
||||
if kayit and kayit[1] == slug and (kayit[0] == tur or not yeni):
|
||||
secilen = kayit[2]
|
||||
else:
|
||||
if istek:
|
||||
t0 = time.time()
|
||||
sonuc = jev_denetim.on_karar(istek, oto, ret, jev_denetim.anahtar())
|
||||
sonuc = jev_denetim.on_karar(istek, sanal, ret, jev_denetim.anahtar())
|
||||
puan = sonuc["puan"]
|
||||
log(f"[{n} {slug}] tur basi karari ({time.time() - t0:.1f} sn): "
|
||||
+ (", ".join(f"{k}={v:.2f}" for k, v in puan.items()) if puan else "karar modeline sorulamadi"))
|
||||
else:
|
||||
sonuc = {"hedef": a["targets"]["zor"] if oto else None, "ret": None}
|
||||
if oto:
|
||||
sonuc = {"hedef": a["targets"]["zor"] if sanal else None, "ret": None}
|
||||
if sanal:
|
||||
hedef = sonuc["hedef"]
|
||||
secilen = IDE_PREFIX + hedef
|
||||
if (p["enabled"] and sonuc["ret"] and not jev_denetim.muaf_mi(hedef) and hedef != p["target"]
|
||||
and hedef not in jev_denetim.sonrakiler(p["target"])):
|
||||
log(f"[{n} {slug}] ret on kontrolu: normal model reddedebilir, adim {p['target']} katmanindan basliyor")
|
||||
secilen = IDE_PREFIX + p["target"]
|
||||
if sohbet:
|
||||
ON_KARAR[sohbet] = (tur, slug, secilen)
|
||||
req["model"] = secilen
|
||||
with KARAR_KILIDI:
|
||||
ON_KARAR.pop(kok, None) # yeniden ekleme sirayi tazeler: aktif sohbet en son silinir
|
||||
ON_KARAR[kok] = (tur, slug, secilen)
|
||||
while len(ON_KARAR) > ON_KARAR_SINIR:
|
||||
ON_KARAR.pop(next(iter(ON_KARAR)))
|
||||
ON_KARAR.pop(next(iter(ON_KARAR)), None)
|
||||
if secilen != slug:
|
||||
log(f"[{n} {slug}] karar: bu tur {secilen} modeline gidiyor")
|
||||
req["model"] = secilen
|
||||
return secilen
|
||||
|
||||
|
||||
def bitis_kullan(tur):
|
||||
"""finishCheck turda en fazla bir kez: ilk cagri True (ve isaretler), sonrakiler False."""
|
||||
with KARAR_KILIDI:
|
||||
if tur in BITIS_TURLARI:
|
||||
return False
|
||||
BITIS_TURLARI[tur] = time.time()
|
||||
while len(BITIS_TURLARI) > ON_KARAR_SINIR:
|
||||
BITIS_TURLARI.pop(next(iter(BITIS_TURLARI)), None)
|
||||
return True
|
||||
|
||||
|
||||
def sigan_katmanlar(sira, req, tag):
|
||||
"""Istegi sigdiramayacak katman zincirden cikar (bos baglam hatasiyla bosa dusmesin)."""
|
||||
"""Istegi sigdiramayacak katman zincirden cikar (bos baglam hatasiyla bosa dusmesin). Yalniz penceresi
|
||||
guvenilir (pencere.bekci_calisir) katman atlanir; zincirin son halkasi hic atlanmaz."""
|
||||
tahmin, sonuc = None, []
|
||||
for ad in sira:
|
||||
p = (IDE_BILGI.get(IDE_PREFIX + ad) or {}).get("pencere")
|
||||
if p:
|
||||
for i, ad in enumerate(sira):
|
||||
bilgi = IDE_BILGI.get(IDE_PREFIX + ad) or {}
|
||||
p = bilgi.get("pencere")
|
||||
if p and i < len(sira) - 1 and pencere.bekci_calisir(bilgi.get("kaynak"), p):
|
||||
if tahmin is None:
|
||||
tahmin = pencere.tahmini_token(req, 2.0)
|
||||
if tahmin > int(p * 0.90):
|
||||
@@ -1059,6 +1107,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
self._akis_basligi(resp, resp.status)
|
||||
kalan, son_yazi, son_govde = list(sira), time.time(), b""
|
||||
devam_kullanildi = False # finishCheck: son cevap ayni modele en fazla bir kez geri verilir
|
||||
yedek = None # geri verilen istek basarisiz olursa teslim edilecek, Jev'den gecmis ilk cevap
|
||||
|
||||
def w(b):
|
||||
if b:
|
||||
@@ -1129,6 +1178,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
yeni_devam = None
|
||||
log(f"{tag} bitis denetimi: devam istegi acilamadi, cevap oldugu gibi geciyor ({type(e).__name__}: {e})")
|
||||
if yeni_devam is not None:
|
||||
yedek = (parcalar, rewriter, tag)
|
||||
resp, rewriter, tag = yeni_devam
|
||||
log(f"{tag} bitis denetimi: is bitmeden bitti denmis, ayni modele bir kez geri verildi")
|
||||
continue
|
||||
@@ -1138,6 +1188,11 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
gecmedi = True
|
||||
log(f"{tag} katman denetimi: bos adim (yazi da komut da yok), siradakine geciliyor: "
|
||||
f"{b''.join(parcalar)[-200:]!r}")
|
||||
if gecmedi and yedek is not None:
|
||||
# Geri verilen istek koptu, bos dondu ya da gecmedi: Jev'den gecmis ilk cevap teslim edilir.
|
||||
parcalar, rewriter, tag = yedek
|
||||
gecmedi = False
|
||||
log(f"{tag} bitis denetimi: geri verilen istek kullanilamadi, ilk cevap teslim ediliyor")
|
||||
yeni = None
|
||||
while gecmedi and kalan and yeni is None:
|
||||
ad = kalan.pop(0)
|
||||
@@ -1670,6 +1725,12 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
slug = karar_asamasi(n, req, sohbet, slug)
|
||||
except Exception as e:
|
||||
log(f"[{n} {slug}] karar asamasi atlandi: {type(e).__name__}: {e}")
|
||||
try:
|
||||
if slug == IDE_PREFIX + OTO_ADI[0]:
|
||||
# Sanal ad gecide gitmez: varsayilan guclu hedef.
|
||||
slug = req["model"] = IDE_PREFIX + jev_settings.DEFAULTS["autoRoute"]["targets"]["zor"]
|
||||
except Exception:
|
||||
pass
|
||||
mapping = MODEL_MAP.get(slug)
|
||||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
||||
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
||||
@@ -1862,14 +1923,19 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
|
||||
# (rewriter yok) yolda kapalidir.
|
||||
jev_rota = ROUTES.get(ide_route)
|
||||
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
|
||||
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
|
||||
try:
|
||||
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
|
||||
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
|
||||
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
|
||||
except Exception as e:
|
||||
# Okunamayan ayar istegi dusurmez: bu adim denetimsiz gecer.
|
||||
log(f"{tag} katman denetimi ayari okunamadi, denetimsiz devam: {type(e).__name__}: {e}")
|
||||
jev_etkin, jev_sira, jev_istek = False, [], ""
|
||||
if jev_sira:
|
||||
try:
|
||||
jev_sira = sigan_katmanlar(jev_sira, req, tag)
|
||||
except Exception as e:
|
||||
log(f"{tag} katman pencere kontrolu atlandi: {type(e).__name__}: {e}")
|
||||
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
|
||||
|
||||
def jev_ac(ad):
|
||||
katman_req = json.loads(data)
|
||||
@@ -1883,9 +1949,12 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
f"[{n} {slug}=>{ad}]")
|
||||
|
||||
def jev_devam(ad, onceki):
|
||||
"""finishCheck: ayni hedefe, erken biten cevap ve not eklenmis istek. Yalniz AITURK gecidindeki hedefler."""
|
||||
"""finishCheck: ayni hedefe, erken biten cevap ve not eklenmis istek. Yalniz AITURK gecidindeki hedefler.
|
||||
Turda en fazla bir kez (bitis_kullan)."""
|
||||
if ad == target and route_name != ide_route:
|
||||
raise ValueError("hedef AITURK gecidinde degil")
|
||||
if not bitis_kullan(bitis_turu):
|
||||
raise ValueError("bu turda zaten geri verildi")
|
||||
devam_req = json.loads(data)
|
||||
devam_req["model"] = ad
|
||||
devam_req.setdefault("input", []).extend([
|
||||
@@ -1900,7 +1969,12 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
|
||||
f"[{n} {slug}=>{ad} devam]")
|
||||
try:
|
||||
bitis_turu = tur_bilgisi(req, sohbet)[1]
|
||||
jev_bitis = jev_devam if jev_etkin and jev_settings.read()[0]["finishCheck"]["enabled"] else None
|
||||
if jev_bitis is not None:
|
||||
with KARAR_KILIDI:
|
||||
if bitis_turu in BITIS_TURLARI:
|
||||
jev_bitis = None # bu turda bir kez geri verildi
|
||||
except Exception:
|
||||
jev_bitis = None
|
||||
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
|
||||
@@ -1994,13 +2068,24 @@ class WatchResponse:
|
||||
def __init__(self, response, accept):
|
||||
self.response, self.filter = response, MetadataFilter(accept)
|
||||
self.done = False
|
||||
self.gecis = False # suzgec tasti (cok buyuk cerceve): kalan akis oldugu gibi gecer
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.response, name)
|
||||
def read1(self, size):
|
||||
if self.gecis:
|
||||
return self.response.read1(size)
|
||||
while not self.done:
|
||||
raw = self.response.read1(size)
|
||||
if raw:
|
||||
data = self.filter.feed(raw)
|
||||
try:
|
||||
data = self.filter.feed(raw)
|
||||
except ValueError:
|
||||
# 256 KB'i asan tek cerceve: akisi dusurmek yerine suzgecsiz devam (yan kanal cercevesi
|
||||
# bu boyda olmaz). Tamponda yan kanal varsa disari cikmaz.
|
||||
data, self.filter.frames.buf = self.filter.frames.buf, b''
|
||||
self.gecis = True
|
||||
log("izleme suzgeci: cok buyuk SSE cercevesi, kalan akis suzgecsiz geciyor")
|
||||
return b'' if b'bekci.watch' in data else data
|
||||
if data: return data
|
||||
else:
|
||||
self.done = True
|
||||
|
||||
Reference in new issue
Block a user