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
@@ -36,25 +36,33 @@ decision always keeps the current path.
|
||||
|
||||
- `jev-settings.json` schema 2 adds `watchModels`, `prefilter`, `autoRoute`, `finishCheck`,
|
||||
`verifierCheck` and `policy`. A schema 1 file is filled from `jev-settings-defaults.json`.
|
||||
The default chain is `Katman-1 → Katman-2 → Katman-3`; `Katman-3`, `Chat-Model` and
|
||||
`bekci-sesli-bekci` always pass.
|
||||
The default chain is `Katman-1 → Katman-2 → Katman-3`; `Katman-3`, `Chat-Model`,
|
||||
`bekci-sesli-bekci` and `Sesli-Hizli-Zeki` always pass.
|
||||
- **Correction channel** (`watchModels`): the Bekçi watch sideband is no longer bound to the hard-coded
|
||||
name `Sesli-Hizli-Zeki`; any exact target in the list owns it.
|
||||
name `Sesli-Hizli-Zeki`; any exact target in the list owns it, but only while it always passes the check
|
||||
(a checked answer may be replaced by a layer, and a receipt must not belong to an answer the user never saw).
|
||||
A single SSE frame above 256 KB no longer breaks the stream; the rest of the stream passes unfiltered.
|
||||
- **Refusal pre-check** (`prefilter`): at the start of a turn, "would a mainstream model refuse or water
|
||||
this down?". If yes, the step starts at `prefilter.target` instead of collecting a refusal first.
|
||||
- **Automatic model** (`autoRoute`): a virtual `ide/<model>` entry ("Otomatik") in the Codex list. Each new
|
||||
user message gets one decision call (hard? research?) and goes to the configured combo. Tool steps of
|
||||
the same turn keep that combo, because switching providers mid-turn breaks encrypted history. The
|
||||
user's explicit model choice is never changed by this stage.
|
||||
- Both turn-start questions go in one call, bounded by `timeoutSeconds` (default 3 s).
|
||||
- **Layer window filter**: a layer whose context window cannot hold the request is skipped.
|
||||
- **Automatic model** (`autoRoute`): a virtual `ide/<model>` entry ("Otomatik") in the Codex list. A new turn
|
||||
(a user message after the previous final answer) gets one decision call (hard? research?) and goes to
|
||||
the configured combo. Tool steps, messages typed while the turn runs, a lone "." and compaction stay on
|
||||
the same combo, because switching providers mid-turn breaks encrypted history. When the stage is off or
|
||||
fails, the virtual name never reaches the gateway: the last choice or `targets.zor` is used. The user's
|
||||
explicit model choice is never changed by this stage.
|
||||
- Both turn-start questions go in one call; the wait, including the local queue, is bounded by
|
||||
`timeoutSeconds` (default 3 s).
|
||||
- **Layer window filter**: a layer whose trusted context window (pencere.bekci_calisir) cannot hold the
|
||||
request is skipped. The last link of the chain is never skipped.
|
||||
- **Early finish check** (`finishCheck`): asked in the same call as the refusal check on a final answer.
|
||||
If the answer stopped early or claimed success without evidence, it goes back to the same model once
|
||||
with `finishCheck.note`; the retry is delivered without another finish question.
|
||||
If the answer stopped early or claimed success without evidence, it goes back to the same model with
|
||||
`finishCheck.note`, at most once per turn. If the retry breaks, comes back empty or fails the check, the
|
||||
first (already approved) answer is delivered.
|
||||
- **Rules from Dify** (`policy.source = dify`, `karar_politikasi.py`): the stage questions, thresholds and
|
||||
combo choices may come from a Dify workflow's `kurallar` output. Fetched in the background every
|
||||
`refreshMinutes`, validated against the local settings, last good copy kept in `jev-policy.json`.
|
||||
The transition chain and the always-pass list stay local. A turn never waits for Dify.
|
||||
The transition chain and the always-pass list stay local. A turn never waits for Dify, and a redirect
|
||||
from the Dify endpoint is refused so the app key never travels to another host.
|
||||
- **Workflow reviewer check** (`verifierCheck`, `karar_sor.py`): the Codex dynamic-workflow acceptance
|
||||
step asks whether a reviewer approved without concrete evidence; if so acceptance is blocked.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{ "from": "Katman-1", "to": "Katman-2" },
|
||||
{ "from": "Katman-2", "to": "Katman-3" }
|
||||
],
|
||||
"exemptModels": ["Chat-Model", "bekci-sesli-bekci", "Katman-3"],
|
||||
"exemptModels": ["Chat-Model", "bekci-sesli-bekci", "Sesli-Hizli-Zeki", "Katman-3"],
|
||||
"thresholds": { "ret": 0.5, "sart": 0.7 },
|
||||
"questions": {
|
||||
"ret": "The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic.",
|
||||
@@ -41,8 +41,7 @@
|
||||
"enabled": false,
|
||||
"question": "The assistant stopped before the user's task was finished: it said it is done or handed back without doing all of what was asked, or it claimed success (tests pass, file changed, deployed) without showing the actual evidence.",
|
||||
"threshold": 0.6,
|
||||
"note": "Önceki cevabın iş bitmeden bitti dedi ya da kanıt göstermedi. İşi gerçekten bitir; gerekiyorsa araçları kullan ve gerçek kanıtı göster.",
|
||||
"timeoutSeconds": 5
|
||||
"note": "Önceki cevabın iş bitmeden bitti dedi ya da kanıt göstermedi. İşi gerçekten bitir; gerekiyorsa araçları kullan ve gerçek kanıtı göster."
|
||||
},
|
||||
"verifierCheck": {
|
||||
"enabled": false,
|
||||
|
||||
@@ -38,11 +38,14 @@ SORU = {
|
||||
_ANAHTAR = {"okundu": False, "deger": ""}
|
||||
|
||||
def izleme_mi(hedef):
|
||||
"""Bekci izleme kanali (cevaptan sonra duzeltme) bu hedefin mi? Ayar: watchModels, tam ad eslesmesi."""
|
||||
"""Bekci izleme kanali (cevaptan sonra duzeltme) bu hedefin mi? Ayar: watchModels, tam ad eslesmesi.
|
||||
Jev acikken yalniz muaf hedefte: denetlenen cevap baska katmana gecebilir, makbuz kullanicinin gormedigi
|
||||
cevaba baglanmasin."""
|
||||
try:
|
||||
return hedef in jev_settings.read()[0]['watchModels']
|
||||
s = jev_settings.read()[0]
|
||||
return hedef in s['watchModels'] and (not s['enabled'] or hedef in s['exemptModels'])
|
||||
except Exception:
|
||||
return hedef == "Sesli-Hizli-Zeki"
|
||||
return False
|
||||
|
||||
|
||||
def muaf_mi(hedef):
|
||||
@@ -125,18 +128,23 @@ def son_istek(req):
|
||||
return son_istek_sirasi(req)[0]
|
||||
|
||||
|
||||
def istek_metni(oge):
|
||||
"""Bir kullanici mesajindaki istek: sesli aktarimda <input> icindeki soz; sarmalli sistem mesaji ise ""."""
|
||||
if not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||||
return ""
|
||||
metin = _metin(oge).strip()
|
||||
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return metin if metin and not metin.startswith("<") else ""
|
||||
|
||||
|
||||
def son_istek_sirasi(req):
|
||||
"""(istek, girdideki sirasi). Turun arac adimlarinda da ayni sira doner; yoksa ("", -1)."""
|
||||
girdi = req.get("input") or []
|
||||
for sira in range(len(girdi) - 1, -1, -1):
|
||||
oge = girdi[sira]
|
||||
if not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||||
continue
|
||||
metin = _metin(oge).strip()
|
||||
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
|
||||
if m:
|
||||
return m.group(1).strip(), sira
|
||||
if metin and not metin.startswith("<"):
|
||||
metin = istek_metni(girdi[sira])
|
||||
if metin:
|
||||
return metin, sira
|
||||
return "", -1
|
||||
|
||||
@@ -202,7 +210,7 @@ def sorular(durum, soru, key, timeout, gonder=_gonder, settings=None):
|
||||
return None
|
||||
govde = {"model": settings['model'], "state": durum,
|
||||
"questions": {k: {"type": "noul", "instructions": v} for k, v in soru.items()}}
|
||||
if not jev_settings.acquire(settings):
|
||||
if not jev_settings.acquire(settings, wait=timeout):
|
||||
return None
|
||||
try:
|
||||
response = gonder(govde, key, settings, timeout) if gonder is _gonder else gonder(govde, key)
|
||||
|
||||
@@ -87,11 +87,11 @@ def validate(settings):
|
||||
and all(_text((a.get('questions') or {}).get(k)) and _prob((a.get('thresholds') or {}).get(k)) for k in ('zor', 'arastirma'))
|
||||
and all(_text((a.get('targets') or {}).get(k), 256) for k in ('basit', 'zor', 'arastirma'))):
|
||||
raise ValueError('Invalid automatic routing')
|
||||
if a['model'] in a['targets'].values():
|
||||
if a['model'] in a['targets'].values() or p['target'] == a['model']:
|
||||
raise ValueError('Automatic routing cannot target itself')
|
||||
f = settings['finishCheck']
|
||||
if not (isinstance(f.get('enabled'), bool) and _text(f.get('question')) and _prob(f.get('threshold'))
|
||||
and _text(f.get('note'), 4000) and _int(f.get('timeoutSeconds'), 1, 60)):
|
||||
and _text(f.get('note'), 4000)):
|
||||
raise ValueError('Invalid finish check')
|
||||
v = settings['verifierCheck']
|
||||
if not (isinstance(v.get('enabled'), bool) and _text(v.get('question')) and _prob(v.get('threshold'))):
|
||||
@@ -186,15 +186,27 @@ def write_policy(result):
|
||||
record['lastGoodAt'] = previous.get('lastGoodAt') or previous.get('fetchedAt')
|
||||
elif result.get('ok'):
|
||||
record['lastGoodAt'] = record['fetchedAt']
|
||||
tmp = path.with_name(path.name + f'.{os.getpid()}.tmp')
|
||||
tmp = path.with_name(path.name + f'.{os.getpid()}.{threading.get_ident()}.tmp')
|
||||
tmp.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
os.replace(tmp, path)
|
||||
try:
|
||||
for deneme in range(5):
|
||||
try:
|
||||
os.replace(tmp, path) # Windows: a reader holding the file makes this fail briefly
|
||||
break
|
||||
except PermissionError:
|
||||
if deneme == 4:
|
||||
raise
|
||||
time.sleep(0.05 * (deneme + 1))
|
||||
finally:
|
||||
if tmp.exists():
|
||||
tmp.unlink()
|
||||
return record
|
||||
|
||||
|
||||
def acquire(settings):
|
||||
def acquire(settings, wait=None):
|
||||
"""wait: at most this many seconds in the queue (turn-start questions must not hold a request)."""
|
||||
global _active
|
||||
deadline = time.monotonic() + settings['queueWaitSeconds']
|
||||
deadline = time.monotonic() + (min(settings['queueWaitSeconds'], wait) if wait else settings['queueWaitSeconds'])
|
||||
with _condition:
|
||||
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
|
||||
remaining = deadline - time.monotonic()
|
||||
|
||||
@@ -13,11 +13,21 @@ jev_settings.POLICY_KEYS, each holding the same fields as the local settings sec
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import jev_settings
|
||||
|
||||
UA = "AITURK-IDE-codex-bridge/karar-politikasi"
|
||||
|
||||
|
||||
class _YonlendirmeYok(urllib.request.HTTPRedirectHandler):
|
||||
"""A redirect would carry the Authorization header to another host; it is an error instead."""
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
raise urllib.error.HTTPError(req.full_url, code, f"redirect refused: {newurl}", headers, fp)
|
||||
|
||||
|
||||
_ACICI = urllib.request.build_opener(_YonlendirmeYok)
|
||||
MAX_BYTES = 64 * 1024
|
||||
_son = {"zaman": 0.0}
|
||||
|
||||
@@ -43,7 +53,7 @@ def ayikla(cikti, yerel):
|
||||
return overrides
|
||||
|
||||
|
||||
def cek(settings, key, urlopen=urllib.request.urlopen):
|
||||
def cek(settings, key, urlopen=_ACICI.open):
|
||||
"""One fetch. Returns {"ok": bool, "overrides": {...}, "error": str}; never raises."""
|
||||
if not key:
|
||||
return {"ok": False, "error": "Dify key not configured"}
|
||||
@@ -70,7 +80,7 @@ def zamani_mi(settings, simdi=None):
|
||||
return settings['policy']['source'] == 'dify' and simdi - _son["zaman"] >= settings['policy']['refreshMinutes'] * 60
|
||||
|
||||
|
||||
def yenile(log=lambda _m: None, urlopen=urllib.request.urlopen, zorla=False):
|
||||
def yenile(log=lambda _m: None, urlopen=_ACICI.open, zorla=False):
|
||||
"""Fetch when due; store the outcome. Local settings are read without the Dify overlay."""
|
||||
try:
|
||||
yerel, _ = _yerel_ayar()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
@@ -205,10 +206,12 @@ class TurBasi(Ev):
|
||||
sahte_denetim.son_istek = jev_denetim.son_istek
|
||||
self.cevap = {'hedef': None, 'ret': None, 'puan': None}
|
||||
sahte_denetim.son_istek_sirasi = jev_denetim.son_istek_sirasi
|
||||
sahte_denetim.istek_metni = jev_denetim.istek_metni
|
||||
self.env = {'IDE_PREFIX': 'ide/', 'ON_KARAR': {}, 'ON_KARAR_SINIR': 3, 'jev_settings': jev_settings,
|
||||
'jev_denetim': sahte_denetim, 'model_gecis': model_gecis, 'hashlib': hashlib, 'time': time,
|
||||
'log': self.logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('karar_asamasi')], type_ignores=[]), 'kopru.py', 'exec'), self.env)
|
||||
'json': json, 'KARAR_KILIDI': threading.Lock(), 'BITIS_TURLARI': {}, 'log': self.logs.append}
|
||||
govde = [fonksiyon(a) for a in ('tur_bilgisi', 'karar_asamasi', 'bitis_kullan')]
|
||||
exec(compile(ast.Module(body=govde, type_ignores=[]), 'kopru.py', 'exec'), self.env)
|
||||
self.karar = self.env['karar_asamasi']
|
||||
|
||||
def on_karar(self, istek, oto, ret, key):
|
||||
@@ -220,8 +223,10 @@ class TurBasi(Ev):
|
||||
|
||||
def test_everything_off_changes_nothing_and_asks_nothing(self):
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
for model in ('ide/Ajan', 'ide/codex-gpt-6-astra', 'ide/Chat-Model'):
|
||||
req = self.istek(model, user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', model), model)
|
||||
self.assertEqual(req['model'], model)
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_automatic_choice_is_kept_for_the_tool_steps_of_the_same_turn(self):
|
||||
@@ -235,7 +240,8 @@ class TurBasi(Ev):
|
||||
req2 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), {'type': 'function_call', 'call_id': 'c1'}, arac)
|
||||
self.assertEqual(self.karar(2, req2, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.assertEqual(len(self.kararlar), 1) # same turn: no second question
|
||||
req3 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), arac, user('simdi kodu duzelt'))
|
||||
son_cevap = {'type': 'message', 'role': 'assistant', 'content': [{'type': 'output_text', 'text': 'Rapor hazir.'}]}
|
||||
req3 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), arac, son_cevap, user('simdi kodu duzelt'))
|
||||
self.cevap = {'hedef': 'Ajan', 'ret': None, 'puan': {'zor': 0.9, 'arastirma': 0.1}}
|
||||
self.assertEqual(self.karar(3, req3, 'sohbet-1', 'ide/Otomatik'), 'ide/Ajan')
|
||||
self.assertEqual(len(self.kararlar), 2) # new user message: new decision
|
||||
@@ -272,9 +278,52 @@ class TurBasi(Ev):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
# The virtual name never leaves the bridge; nothing is asked while the main switch is off.
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/' + self.settings['autoRoute']['targets']['zor'])
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_a_message_typed_mid_turn_or_a_dot_keeps_the_choice(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': 'Arastirma', 'ret': None, 'puan': {'zor': 0.2, 'arastirma': 0.9}}
|
||||
bas = user('kaynaklari karsilastir')
|
||||
cagri = {'type': 'function_call', 'call_id': 'c1', 'name': 'shell', 'arguments': '{}'}
|
||||
cikti = {'type': 'function_call_output', 'call_id': 'c1', 'output': 'ok'}
|
||||
self.assertEqual(self.karar(1, self.istek('ide/Otomatik', bas), 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.cevap = {'hedef': 'Workflow-Kesif', 'ret': None, 'puan': {'zor': 0.1, 'arastirma': 0.1}}
|
||||
for ek in (user('.'), user('bir de sunu ekle'), {'type': 'compaction_trigger'}):
|
||||
req = self.istek('ide/Otomatik', bas, cagri, cikti, ek)
|
||||
self.assertEqual(self.karar(2, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma', ek)
|
||||
self.assertEqual(len(self.kararlar), 1)
|
||||
|
||||
def test_a_disabled_automatic_model_never_reaches_the_gateway(self):
|
||||
self.save() # autoRoute off
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
secilen = self.karar(1, req, 'sohbet-1', 'ide/Otomatik')
|
||||
self.assertEqual(secilen, 'ide/' + self.settings['autoRoute']['targets']['zor'])
|
||||
self.assertEqual(req['model'], secilen)
|
||||
self.assertEqual(self.kararlar, [])
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.settings['enabled'] = False # main switch off
|
||||
self.save()
|
||||
self.assertNotEqual(self.karar(2, self.istek('ide/Otomatik', user('yap')), 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
|
||||
def test_without_a_conversation_key_the_tool_steps_still_keep_the_choice(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': 'Ajan', 'ret': None, 'puan': {'zor': 0.9, 'arastirma': 0.1}}
|
||||
bas = user('kodu duzelt')
|
||||
self.assertEqual(self.karar(1, {'model': 'ide/Otomatik', 'input': [bas]}, '', 'ide/Otomatik'), 'ide/Ajan')
|
||||
cikti = {'type': 'function_call_output', 'call_id': 'c1', 'output': 'ok'}
|
||||
req = {'model': 'ide/Otomatik', 'input': [bas, {'type': 'function_call', 'call_id': 'c1'}, cikti]}
|
||||
self.assertEqual(self.karar(2, req, '', 'ide/Otomatik'), 'ide/Ajan')
|
||||
self.assertEqual(len(self.kararlar), 1)
|
||||
|
||||
def test_the_finish_retry_is_used_once_per_turn(self):
|
||||
self.assertTrue(self.env['bitis_kullan']('tur-1'))
|
||||
self.assertFalse(self.env['bitis_kullan']('tur-1'))
|
||||
self.assertTrue(self.env['bitis_kullan']('tur-2'))
|
||||
|
||||
def test_turn_memory_is_bounded(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
@@ -285,15 +334,25 @@ class TurBasi(Ev):
|
||||
|
||||
|
||||
class KatmanPenceresi(unittest.TestCase):
|
||||
def test_a_layer_too_small_for_the_request_is_skipped(self):
|
||||
def suzgec(self, bilgi):
|
||||
logs = []
|
||||
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': {'ide/Katman-2': {'pencere': 1000}, 'ide/Katman-3': {'pencere': 10 ** 6}},
|
||||
'pencere': pencere, 'log': logs.append}
|
||||
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': bilgi, 'pencere': pencere, 'log': logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('sigan_katmanlar')], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
return env['sigan_katmanlar'], logs
|
||||
|
||||
def test_a_layer_too_small_for_the_request_is_skipped(self):
|
||||
f, logs = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'katalog'}, 'ide/Katman-3': {'pencere': 10 ** 6}})
|
||||
req = {'input': [user('x' * 20000)]}
|
||||
self.assertEqual(env['sigan_katmanlar'](['Katman-2', 'Katman-3', 'Bilinmeyen'], req, '[1]'), ['Katman-3', 'Bilinmeyen'])
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3', 'Bilinmeyen'], req, '[1]'), ['Katman-3', 'Bilinmeyen'])
|
||||
self.assertIn('Katman-2', logs[0])
|
||||
|
||||
def test_the_last_layer_and_untrusted_windows_are_never_skipped(self):
|
||||
req = {'input': [user('x' * 20000)]}
|
||||
f, _ = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'katalog'}, 'ide/Katman-3': {'pencere': 1000, 'kaynak': 'katalog'}})
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3'], req, '[1]'), ['Katman-3']) # last one stays
|
||||
f, _ = self.suzgec({'ide/Katman-2': {'pencere': 1000, 'kaynak': 'varsayilan'}})
|
||||
self.assertEqual(f(['Katman-2', 'Katman-3'], req, '[1]'), ['Katman-2', 'Katman-3']) # placeholder window
|
||||
|
||||
|
||||
class BitisDenetimi(Ev):
|
||||
"""_denetimli_akis with finishCheck: an answer that stops early goes back to the same model once."""
|
||||
@@ -372,6 +431,17 @@ class BitisDenetimi(Ev):
|
||||
self.h._denetimli_akis(Yanit(sse('Bitti.')), '[1]', None, None, 'yap', [], Mock(), hedef='Ajan', devam=bozuk)
|
||||
self.assertIn(b'Bitti.', self.gonderilen())
|
||||
|
||||
def test_a_broken_or_refused_retry_delivers_the_approved_first_answer(self):
|
||||
for ikinci in (sse(), sse('Yapamam.')):
|
||||
with self.subTest(ikinci=ikinci[:40]):
|
||||
self.setUp()
|
||||
self.puanlar = [{'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.9}, {'ret': 0.9, 'sart': 0.0}]
|
||||
ac = Mock(return_value=(Yanit(sse('katman')), None, '[K1]'))
|
||||
self.h._denetimli_akis(Yanit(sse('Ilk cevap.')), '[1]', None, None, 'yap', ['Katman-1'], ac,
|
||||
hedef='Ajan', devam=lambda ad, onceki: (Yanit(ikinci), None, '[devam]'))
|
||||
self.assertIn(b'Ilk cevap.', self.gonderilen())
|
||||
ac.assert_not_called()
|
||||
|
||||
def test_without_the_stage_the_finish_question_is_not_sent(self):
|
||||
govdeler = []
|
||||
|
||||
@@ -387,9 +457,6 @@ class BitisDenetimi(Ev):
|
||||
self.assertFalse(jev_denetim.bitmedi_mi(puan))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
class DogrulayiciElemesi(Ev):
|
||||
def test_off_by_default_and_asked_only_when_enabled(self):
|
||||
@@ -416,3 +483,68 @@ class DogrulayiciElemesi(Ev):
|
||||
import io as _io
|
||||
import karar_sor
|
||||
self.assertEqual(karar_sor.main(['baska'], _io.StringIO('{}'), _io.StringIO()), 2)
|
||||
|
||||
|
||||
class Inceleme(Ev):
|
||||
def test_the_correction_channel_needs_an_always_pass_target_while_checks_are_on(self):
|
||||
self.settings['watchModels'] = ['Chat-Model', 'Ajan']
|
||||
self.save()
|
||||
self.assertTrue(jev_denetim.izleme_mi('Chat-Model'))
|
||||
self.assertFalse(jev_denetim.izleme_mi('Ajan')) # checked target: no sideband
|
||||
self.settings['enabled'] = False
|
||||
self.save()
|
||||
self.assertTrue(jev_denetim.izleme_mi('Ajan'))
|
||||
|
||||
def test_a_turn_start_question_does_not_wait_in_the_queue_beyond_its_timeout(self):
|
||||
self.settings['maxConcurrent'] = 1
|
||||
self.settings['queueWaitSeconds'] = 3600
|
||||
self.save()
|
||||
s = jev_settings.read()[0]
|
||||
self.assertTrue(jev_settings.acquire(s)) # the only slot is taken
|
||||
try:
|
||||
t0 = time.monotonic()
|
||||
self.assertIsNone(jev_denetim.sorular({'user_request': 'x'}, {'zor': 'q'}, 'k', 1, gonder=lambda g, k: {}))
|
||||
self.assertLess(time.monotonic() - t0, 3)
|
||||
finally:
|
||||
jev_settings.release()
|
||||
|
||||
def test_the_prefilter_target_cannot_be_the_automatic_model(self):
|
||||
self.settings['prefilter']['target'] = self.settings['autoRoute']['model']
|
||||
self.save()
|
||||
with self.assertRaises(ValueError):
|
||||
jev_settings.read()
|
||||
|
||||
def test_dify_redirects_are_refused_so_the_key_never_moves(self):
|
||||
import urllib.error
|
||||
with self.assertRaises(urllib.error.HTTPError):
|
||||
karar_politikasi._YonlendirmeYok().redirect_request(
|
||||
Mock(full_url='https://dify.example/v1/workflows/run'), None, 302, 'Found', {}, 'http://evil.example/')
|
||||
|
||||
|
||||
class IzlemeSuzgeci(unittest.TestCase):
|
||||
def test_a_huge_frame_passes_through_instead_of_breaking_the_stream(self):
|
||||
tree_sinif = next(n for n in TREE.body if isinstance(n, ast.ClassDef) and n.name == 'WatchResponse')
|
||||
from bekci_watch import MetadataFilter
|
||||
logs = []
|
||||
env = {'MetadataFilter': MetadataFilter, 'log': logs.append}
|
||||
exec(compile(ast.Module(body=[tree_sinif], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
buyuk = b'data: ' + b'x' * 300000
|
||||
parcalar = [buyuk[:200000], buyuk[200000:], b'\n\n', b'']
|
||||
|
||||
class Ham:
|
||||
def read1(self, n):
|
||||
return parcalar.pop(0)
|
||||
w = env['WatchResponse'](Ham(), lambda m: None)
|
||||
cikti = b''
|
||||
while True:
|
||||
b = w.read1(65536)
|
||||
if not b and w.done or not parcalar and not b:
|
||||
break
|
||||
cikti += b
|
||||
self.assertIn(b'x' * 1000, cikti)
|
||||
self.assertEqual(cikti.count(b'x'), 300000)
|
||||
self.assertTrue(logs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -23,7 +23,7 @@ export interface StagedTransitionSettings {
|
||||
thresholds: { zor: number; arastirma: number }
|
||||
targets: { basit: string; zor: string; arastirma: string }
|
||||
}
|
||||
finishCheck: { enabled: boolean; question: string; threshold: number; note: string; timeoutSeconds: number }
|
||||
finishCheck: { enabled: boolean; question: string; threshold: number; note: string }
|
||||
verifierCheck: { enabled: boolean; question: string; threshold: number }
|
||||
policy: { source: 'local' | 'dify'; endpoint: string; refreshMinutes: number }
|
||||
}
|
||||
|
||||
@@ -106,5 +106,12 @@ describe('staged transition settings', () => {
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, endpoint: 'http://dify.example' } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, source: 'web' } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, watchModels: ['a', 'a'] })).toThrow()
|
||||
for (const endpoint of ['https:dify.example/v1', 'https:/dify.example', 'https:///dify.example', 'ftp://dify.example']) {
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, endpoint } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, endpoint })).toThrow()
|
||||
}
|
||||
expect(() =>
|
||||
validateStagedTransition({ ...base, prefilter: { ...base.prefilter, target: base.autoRoute.model } })
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -53,13 +53,21 @@ const probability = (n: unknown): n is number => typeof n === 'number' && Number
|
||||
const integer = (n: unknown, min: number, max: number): n is number =>
|
||||
typeof n === 'number' && Number.isInteger(n) && n >= min && n <= max
|
||||
|
||||
// Same acceptance as the bridge (urllib.parse.urlsplit): the literal https:// prefix and a host. WHATWG URL alone
|
||||
// also accepts "https:host" and "https:/host", which the bridge rejects.
|
||||
function https(value: unknown): value is string {
|
||||
if (!text(value, 2048)) {
|
||||
if (!text(value, 2048) || !/^https:\/\/[^/?#\s]/i.test(value.trim())) {
|
||||
return false
|
||||
}
|
||||
const url = new URL(value)
|
||||
let url: URL
|
||||
|
||||
return url.protocol === 'https:' && !url.username && !url.password && !url.hash
|
||||
try {
|
||||
url = new URL(value.trim())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return url.protocol === 'https:' && Boolean(url.hostname) && !url.username && !url.password && !url.hash
|
||||
}
|
||||
|
||||
export function validateStagedTransition(input: unknown): StagedTransitionSettings {
|
||||
@@ -160,7 +168,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
throw new Error('Invalid automatic routing')
|
||||
}
|
||||
|
||||
if (Object.values(a.targets).some(t => t.trim() === a.model.trim())) {
|
||||
if (Object.values(a.targets).some(t => t.trim() === a.model.trim()) || p.target.trim() === a.model.trim()) {
|
||||
throw new Error('Automatic routing cannot target itself')
|
||||
}
|
||||
|
||||
@@ -170,8 +178,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
typeof f?.enabled !== 'boolean' ||
|
||||
!text(f.question) ||
|
||||
!probability(f.threshold) ||
|
||||
!text(f.note, 4000) ||
|
||||
!integer(f.timeoutSeconds, 1, 60)
|
||||
!text(f.note, 4000)
|
||||
) {
|
||||
throw new Error('Invalid finish check')
|
||||
}
|
||||
@@ -214,13 +221,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
thresholds: { zor: a.thresholds.zor, arastirma: a.thresholds.arastirma },
|
||||
targets: { basit: a.targets.basit.trim(), zor: a.targets.zor.trim(), arastirma: a.targets.arastirma.trim() }
|
||||
},
|
||||
finishCheck: {
|
||||
enabled: f.enabled,
|
||||
question: f.question,
|
||||
threshold: f.threshold,
|
||||
note: f.note,
|
||||
timeoutSeconds: f.timeoutSeconds
|
||||
},
|
||||
finishCheck: { enabled: f.enabled, question: f.question, threshold: f.threshold, note: f.note },
|
||||
verifierCheck: { enabled: c.enabled, question: c.question, threshold: c.threshold },
|
||||
policy: { source: pol.source, endpoint: pol.endpoint.trim(), refreshMinutes: pol.refreshMinutes }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user