feat(codex-bridge): per-step check with Jev, blocked steps go to a fallback model

User decision (11 Oct 2026): every step of an added model is checked in the
bridge; Jev only passes or blocks; a blocked step is redone by
Uncensored-Uygulama and the next step goes back to the selected model.

The bridge buffers the step's answer, asks Jev four yes/no questions and
decides in code: blocked when the answer refuses or dodges the request, or
changes it with its own conditions without asking; passed when it reports a
concrete technical obstacle, asks the user a question, or has no text (tool
calls only). While buffering the client gets SSE comment lines. Jev unreachable
or fallback unavailable: the first answer is delivered. Codex's own models do
not reach the bridge and are not checked.

Also logs the shape of each forwarded request (not its content): the recurring
"upstream 400" turned out to be Codex's trailing configuration_update item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 07:56:27 +03:00
1 parent c8993d58cc
commit 5556ad11af
6 files changed
+451 -5

No files matched your search

+95 -2
View File
@@ -30,6 +30,7 @@ import yarim
import model_gecis
import is_hatti
import internet
import jev_denetim
SURUM = "8"
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -862,13 +863,73 @@ class H(http.server.BaseHTTPRequestHandler):
n = int(self.headers.get("Content-Length") or 0)
return self.rfile.read(n) if n else b""
def _stream(self, resp, status, tag, rewriter=None, izle=None):
def _akis_basligi(self, resp, status):
self.send_response(status)
for k, v in resp.headers.items():
if k.lower() in RESP_HOP: continue
self.send_header(k, v)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
def _denetimli_akis(self, resp, tag, rewriter, izle, istek, ayar, yedek):
"""Adim denetimi (jev_denetim.py): cevap istemciye verilmeden once tamami okunur ve Jev'e sorulur.
Gecerse oldugu gibi verilir; gecmezse atilir ve ayni adimi yedek model yapar (yedek(): (resp, rewriter, tag)).
Beklerken istemciye SSE yorum satiri gider ki Codex akisi kopmus saymasin."""
self._akis_basligi(resp, resp.status)
parcalar, okuyan, son_yazi = [], yarim.Izleyici("", ""), time.time()
def w(b):
if b:
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
try:
while True:
try:
chunk = resp.read1(4096)
except (OSError, http.client.HTTPException) as e:
log(f"{tag} ust kaynak koptu (denetim tamponu, {sum(map(len, parcalar))} bayt): {type(e).__name__}: {e}")
self.close_connection = True
return
if not chunk:
break
parcalar.append(chunk)
okuyan.feed(chunk)
if time.time() - son_yazi >= 10:
w(b": denetim\n\n"); son_yazi = time.time()
metin = okuyan.metin.strip()
gecmedi = False
if metin and istek:
t0 = time.time()
puan = jev_denetim.sor(istek, metin, jev_denetim.anahtar(), float(ayar.get("sure") or jev_denetim.SURE))
if puan is None:
log(f"{tag} adim denetimi: Jev'e sorulamadi, cevap oldugu gibi geciyor")
else:
gecmedi = jev_denetim.karar(puan, ayar)
log(f"{tag} adim denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f} teknik={puan['teknik']:.2f} "
f"soru={puan['soru']:.2f} -> {'GECMEDI' if gecmedi else 'gecti'} ({time.time() - t0:.1f} sn, {len(metin)} karakter)")
if gecmedi:
try:
yeni = yedek()
except Exception as e:
yeni = None
log(f"{tag} adim denetimi: yedek model acilamadi ({type(e).__name__}: {str(e)[:120]}); ilk cevap veriliyor")
if yeni is not None:
resp2, rewriter2, tag2 = yeni
log(f"{tag} adim denetimi: adim yedek modele verildi -> {tag2}")
return self._stream(resp2, resp2.status, tag2, rewriter2, izle, basliksiz=True)
for chunk in parcalar:
if izle is not None:
izle.feed(chunk)
w(rewriter.feed(chunk) if rewriter else chunk)
if rewriter: w(rewriter.flush())
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
log(f"{tag} istemci baglantiyi kapatti (denetim tamponu)")
return
log(f"{tag} bitti status={resp.status} {sum(map(len, parcalar))} bayt")
def _stream(self, resp, status, tag, rewriter=None, izle=None, basliksiz=False):
if not basliksiz:
self._akis_basligi(resp, status)
total = 0
bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir
def w(b):
@@ -950,7 +1011,8 @@ class H(http.server.BaseHTTPRequestHandler):
return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": durum,
"ide_model": int((sayi.split() or ["0"])[0]),
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
"is_hatti": bool(HAT_KOMUT[0])})
"is_hatti": bool(HAT_KOMUT[0]),
"adim_denetimi": bool(jev_denetim.ayar_oku(CFG.get("jev_denetim"))[0] and jev_denetim.anahtar())})
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
self._send_json(200, {"ok": True})
log("AITURK IDE istegiyle kapatiliyor")
@@ -1487,10 +1549,39 @@ class H(http.server.BaseHTTPRequestHandler):
"Daha büyük pencereli bir model seçin ya da mesajı kısaltın.")
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt")
try:
# Istegin bicimi (icerik degil): gecidin reddettigi istegi sonradan ayirt edebilmek icin.
son = (req.get("input") or [None])[-1]
bicim = {k: v for k, v in req.items() if k not in ("input", "tools", "instructions", "prompt_cache_key")}
log(f"{tag} bicim={json.dumps(bicim, ensure_ascii=False)[:420]} son_oge="
f"{(son.get('type'), son.get('role')) if isinstance(son, dict) else type(son).__name__}"
+ (f" icerik={json.dumps(son, ensure_ascii=False)[:700]}" if isinstance(son, dict) and son.get("type") not in
("message", "function_call_output", "custom_tool_call_output", "function_call", "reasoning") else ""))
except Exception:
pass
if DEBUG:
open(os.path.join(LOGDIR, f"{n:04d}_req.json"), "wb").write(data)
hdrs = {"Authorization": f"Bearer {route['key']}", "Content-Type": "application/json", "Accept": "text/event-stream", "User-Agent": UA}
last = None
# Adim denetimi (jev_denetim.py): Jev'in gecirmedigi adimi yedek model yapar. Yedek ayni gecitteyse ve
# secili model zaten yedek degilse acilir; ozet isteginde ve Jev anahtari yokken kapalidir.
jev_yedek = None
jev_acik, jev_hedef, jev_ayar = jev_denetim.ayar_oku(CFG.get("jev_denetim"))
jev_esleme = MODEL_MAP.get(jev_hedef)
if (jev_acik and not ozet_tetik and slug != jev_hedef and jev_esleme and jev_esleme[0] == route_name
and rewriter is not None and jev_denetim.anahtar()):
jev_istek = jev_denetim.son_istek(req)
def jev_yedek():
yedek_req = json.loads(data)
yedek_req["model"] = jev_esleme[1]
yanit = urllib.request.urlopen(urllib.request.Request(
route["url"], data=json.dumps(yedek_req, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST"),
timeout=UPSTREAM_IDLE_TIMEOUT)
return (yanit, SseRewriter(custom, hedef=jev_esleme[1], tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren),
f"[{n} {route_name.upper()} {slug}=>{jev_esleme[1]}]")
if not jev_istek:
jev_yedek = None
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
# only failures before the work starts (no connection, 500/502/503/529) are retried.
@@ -1543,6 +1634,8 @@ class H(http.server.BaseHTTPRequestHandler):
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
if jev_yedek is not None and resp.status == 200:
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_ayar, jev_yedek)
return self._stream(resp, resp.status, tag, rewriter, izle)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})