fix(codex-bridge): remaining audit findings (routing, trimming, retries, model list fallback, logging)

Second batch from the ten read-only audits; 126 bridge tests. Among them: a spoken number
("opus beş beş") resolves, a used route no longer expires after 7 days, the local reply says how to
go back, an unrouted 9r/ name gets a local 503 instead of going to chatgpt.com, a combo request is not
resent after the upstream closed it, the last model list is served when chatgpt.com is unreachable,
trimming keeps the real last user message, Claude-member combos use the Claude estimate, CRLF SSE is
handled, log lines carry the date and the status endpoint reports a code fingerprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 01:50:08 +03:00
1 parent e2793fa38f
commit 7c0dc467d5
10 files changed
+731 -101

No files matched your search

+140 -76
View File
@@ -32,6 +32,11 @@ import is_hatti
SURUM = "8"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] # ayni surum numarasi altindaki farkli kodu ayirt eder
except OSError:
KOD = ""
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
# interpreters the matching `zstandard` build ships next to this file.
@@ -97,8 +102,13 @@ def is_timeout(error):
"""True for a socket/read timeout, also when urllib wraps it in URLError."""
reason = getattr(error, "reason", None)
return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError)
def kopma_mi(error):
"""Baglanti kurulduktan sonra uzak uc kapatti ya da sifirladi (yanit basliklari gelmeden): istek ulasmis ve
is baslamis olabilir. Baglanti hic kurulamadiysa (reddedildi, DNS) False."""
reason = getattr(error, "reason", None)
turler = (ConnectionResetError, ConnectionAbortedError, http.client.IncompleteRead) # RemoteDisconnected dahil
return isinstance(error, turler) or isinstance(reason, turler)
DEBUG = bool(CFG.get("debug", False))
DISPLAY_MAP = CFG.get("display_map", {})
UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir.
NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"])
@@ -108,7 +118,7 @@ lock = threading.Lock()
seq = [0]
def log(msg):
line = time.strftime("%H:%M:%S ") + msg
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
print(line, flush=True)
with lock:
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
@@ -202,7 +212,7 @@ def load_ide_catalog(ag=True):
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels,
"kind": g["kind"], "context_window": g.get("pencere")})
IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"),
"kaynak": g.get("pencere_kaynak") or ""}
"kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))}
model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1
else:
@@ -251,7 +261,7 @@ def save_last_models(acct, hdrs, body):
yol = _cache_path(acct)
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
"body": body.decode("utf-8")}
gecici = yol + ".tmp"
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # ayni hesabin iki es zamanli kaydi carpismasin
with open(gecici, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol) # yarim dosya birakmamak icin
@@ -529,34 +539,47 @@ class SseRewriter:
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
"status_code": d.get("status_code")}}
if t == "error" and isinstance(d.get("error"), dict):
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
metin = str(d["error"].get("message") or d["error"].get("type") or "")
if pencere.aracsiz_mi(metin):
if self.aracsiz is not None:
self.aracsiz(self.hedef)
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
if self.dolu and pencere.kapali_hata_mi(metin):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message":
"Sohbet seçili modelin bağlam penceresine sığmadı. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}]
if pencere.tasma_mi(metin):
if self.tasma is not None:
self.tasma(self.hedef, metin)
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
f"Ayrıntı: {metin[:200]}")
if pencere.kapali_hata_mi(metin):
return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
"yeniden deneyin.")
cevrilen = self._hata_cevir(d)
if cevrilen is not None:
# Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir.
once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else []
return once + cevrilen
return self._sirala_normal(d, t)
def _hata_cevir(self, d):
"""Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir."""
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
metin = str(d["error"].get("message") or d["error"].get("type") or "")
if pencere.aracsiz_mi(metin):
if self.aracsiz is not None:
self.aracsiz(self.hedef)
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
if self.dolu and pencere.kapali_hata_mi(metin):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message":
"Geçit isteği reddetti ve nedenini bildirmedi. Sohbet seçili modelin bağlam penceresine çok yakın; "
"sığmamış olabilir. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}]
if pencere.tasma_mi(metin):
if self.tasma is not None:
self.tasma(self.hedef, metin)
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
f"Ayrıntı: {metin[:200]}")
if pencere.kapali_hata_mi(metin):
return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
"yeniden deneyin.")
return None
def _sirala_normal(self, d, t):
item = d.get("item") if isinstance(d.get("item"), dict) else None
if item and item.get("type") in ARAC_OGELERI:
self.arac_goruldu = True
@@ -613,6 +636,8 @@ class SseRewriter:
return d
def feed(self, chunk):
self.buf += chunk
if b"\r" in self.buf:
self.buf = self.buf.replace(b"\r\n", b"\n") # CRLF ile ayrilan SSE de cevrilir
out = b""
while b"\n\n" in self.buf:
block, self.buf = self.buf.split(b"\n\n", 1)
@@ -624,6 +649,8 @@ class SseRewriter:
d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx))
except Exception:
out += block + b"\n\n"; continue
if not isinstance(d, dict):
out += block + b"\n\n"; continue # "data: null" gibi govde akisi kesmez, oldugu gibi gecer
d = self._fix(d)
if d is None:
continue
@@ -757,7 +784,10 @@ class H(http.server.BaseHTTPRequestHandler):
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
out = bytearray()
while True:
line = self.rfile.readline().strip()
raw = self.rfile.readline()
if not raw:
break # baglanti kapandi (EOF): sonsuz donguye girme
line = raw.strip()
if not line:
continue
size = int(line.split(b";")[0], 16)
@@ -856,7 +886,7 @@ class H(http.server.BaseHTTPRequestHandler):
self.command = "GET"
if self.command == "GET" and path.rstrip("/").endswith("/status"):
durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ")
return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "zstd": ZSTD_OK, "ide_durum": durum,
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))})
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
@@ -882,7 +912,7 @@ class H(http.server.BaseHTTPRequestHandler):
return self._control(path)
if DEBUG:
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
if self.command == "POST" and path.rstrip("/").endswith("/responses"):
if self.command == "POST" and path.split("?", 1)[0].rstrip("/").endswith("/responses"):
return self._responses(n, path, body)
return self._passthrough(n, path, body)
@@ -900,23 +930,19 @@ class H(http.server.BaseHTTPRequestHandler):
except urllib.error.HTTPError as e:
resp = e; status = e.code
except Exception as e:
log(f"{tag} HATA {e}"); return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
log(f"{tag} HATA {e}")
# Ag yokken de model secici bosalmasin: chatgpt.com'a hic ulasilamazsa son basarili liste verilir.
if self.command == "GET" and "/models" in path and self._son_listeyi_ver(tag, self.headers.get("chatgpt-account-id") or "-"):
return
return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
if self.command == "GET" and "/models" in path:
acct = self.headers.get("chatgpt-account-id") or "-"
tok = (self.headers.get("Authorization") or "")[-8:]
if status == 200:
out = self._models(resp, tag)
return out
body = resp.read()[:300]
log(f"{tag} chatgpt.com {status} verdi (hesap={acct} token=..{tok}) govde={body[:160]!r}")
cached = LAST_MODELS.get(acct)
if cached:
self.send_response(200)
for k, v in cached["headers"]:
self.send_header(k, v)
self.send_header("Content-Length", str(len(cached["body"])))
self.end_headers(); self.wfile.write(cached["body"])
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}")
if self._son_listeyi_ver(tag, acct):
return
self.send_response(status)
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
@@ -941,6 +967,19 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
self._stream(resp, status, tag, rewriter, izle)
def _son_listeyi_ver(self, tag, acct):
"""Bu hesabin son basarili model listesini gonderir. True: gonderildi."""
cached = LAST_MODELS.get(acct)
if not cached:
return False
self.send_response(200)
for k, v in cached["headers"]:
self.send_header(k, v)
self.send_header("Content-Length", str(len(cached["body"])))
self.end_headers(); self.wfile.write(cached["body"])
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
return True
def _openai_ozet_coz(self, n, oge, ozet_cfg):
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
klasor = os.path.join(LOGDIR, "ozet-onbellek")
@@ -1006,12 +1045,11 @@ class H(http.server.BaseHTTPRequestHandler):
data = json.loads(raw)
models = data.get("models", [])
changed = 0
for m in models:
new = DISPLAY_MAP.get(m.get("slug"))
if new:
m["display_name"] = new; changed += 1
# Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz).
by_slug = {m.get("slug"): m for m in models}
for x in EXTRA_MODELS:
ozgun_kimlikler = set(by_slug)
ekler = EXTRA_MODELS # tek kopya: liste hazirlanirken katalog yenilenirse sira ve icerik tutarli kalir
for x in ekler:
if x["slug"] in by_slug:
continue
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
@@ -1051,13 +1089,14 @@ class H(http.server.BaseHTTPRequestHandler):
if not m.get("base_instructions") and not has_tmpl:
m.update(saved)
models.append(m); changed += 1
if EXTRA_MODELS:
ide_catalog.sirala_codex(models, EXTRA_MODELS)
if ekler:
ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler)
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}")
out = raw; changed = 0
raw_len = len(out)
duz = out # sikistirilmamis govde: yedege bu yazilir
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
enc_out = None
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
@@ -1070,15 +1109,16 @@ class H(http.server.BaseHTTPRequestHandler):
if kl in ("etag", "x-models-etag") and changed:
v = catalog_etag
hdrs_out.append((k, v))
if enc_out:
hdrs_out.append(("Content-Encoding", enc_out))
if changed and not enc_out:
if changed:
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
acct_ = self.headers.get("chatgpt-account-id") or "-"
onceki = LAST_MODELS.get(acct_)
LAST_MODELS[acct_] = {"headers": hdrs_out, "body": out, "at": time.strftime("%H:%M:%S")}
LAST_MODELS[acct_] = {"headers": list(hdrs_out), "body": duz, "at": time.strftime("%Y-%m-%d %H:%M:%S")}
# Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz.
if not onceki or onceki.get("body") != out:
save_last_models(acct_, hdrs_out, out)
if not onceki or onceki.get("body") != duz:
save_last_models(acct_, list(hdrs_out), duz)
if enc_out:
hdrs_out.append(("Content-Encoding", enc_out))
self.send_response(200)
for k, v in hdrs_out:
self.send_header(k, v)
@@ -1120,6 +1160,7 @@ class H(http.server.BaseHTTPRequestHandler):
return soru
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; "
"model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]")
self.komut_govdeyi_degistirdi = True
return None
hedef = sonuc["slug"]
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
@@ -1131,9 +1172,12 @@ class H(http.server.BaseHTTPRequestHandler):
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
if ortada:
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
self.komut_govdeyi_degistirdi = True
return None
# Listede ayni modeli yeniden secmek yonlendirmeyi kaldirmaz (Codex ayni adi gonderir); geri donus yolu soylenir.
donus = f" Listede seçili modele dönmek için \"modeli {model_adi(gelen)} yap\" de." if hedef != gelen else ""
return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. "
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.")
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}")
def _responses(self, n, path, body):
try:
@@ -1147,6 +1191,7 @@ class H(http.server.BaseHTTPRequestHandler):
once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)
sohbet = str(req.get("prompt_cache_key") or "")
if CFG.get("sesle_model", True) and sohbet:
self.komut_govdeyi_degistirdi = False
try:
yerel = self._model_komutu(n, req, sohbet, gelen)
except Exception as e:
@@ -1154,8 +1199,10 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
if yerel is not None:
return self._yerel_yanit(gelen, yerel)
if self.komut_govdeyi_degistirdi:
govde_degisti = True # komut mesaji nota cevrildi: abonelik yolunda da degismis govde gider
try:
if model_gecis.gecmisi_temizle(req, bilinen_modeller()):
if model_gecis.gecmisi_temizle(req, bilinen_modeller(), YONLENDIRME.al(sohbet, gelen) or gelen):
govde_degisti = True
except Exception as e:
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
@@ -1176,7 +1223,7 @@ class H(http.server.BaseHTTPRequestHandler):
# Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da
# istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur.
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
return self._yerel_yanit(gelen, f"{model_adi(slug)} hazır. Sohbetin modeli artık {model_adi(slug)}.")
return self._yerel_yanit(gelen, f"{model_adi(slug)} seçildi. Sohbetin modeli artık {model_adi(slug)}.")
# Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py).
kaynak = self.headers.get("originator") or ""
if kaynak and kaynak not in GORULEN_ISTEMCI:
@@ -1206,10 +1253,15 @@ class H(http.server.BaseHTTPRequestHandler):
ozet_acik = ozet_cfg.get("enabled", False)
catalog = CFG.get("nine_router_catalog") or {}
prefix = catalog.get("prefix", "9r/")
if mapping is None and catalog.get("enabled") and slug.startswith(prefix):
# Newly added API models can be selected before the background refresh.
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
if mapping is None and prefix and slug.startswith(prefix):
if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES:
# Newly added API models can be selected before the background refresh.
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
else:
# Rotasi kalmamis eski ad chatgpt.com'a baska bir adla gonderilmez ("model desteklenmiyor" donuyordu).
log(f"[{n} {slug}] bu on ekin rotasi yok; istek gonderilmedi")
return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}})
ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
mapping = (ide_route, slug[len(ide_prefix):], None)
@@ -1287,7 +1339,11 @@ class H(http.server.BaseHTTPRequestHandler):
return
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
via_router = route_name in (catalog.get("route", "9router"), ide_route)
route = ROUTES[route_name]
route = ROUTES.get(route_name)
if route is None:
# Katalog yenilemesi rotayi bu istek hazirlanirken kaldirdi (baglama kapandi ya da IDE oturumu gitti).
log(f"[{n} {slug}] rota artik yok: {route_name}")
return self._send_json(503, {"error": {"message": "Bu modelin baglantisi su an kapali; birazdan yeniden deneyin ya da baska bir model secin."}})
tag = f"[{n} {route_name.upper()} {slug}->{target}]"
req["model"] = target
req.pop("client_metadata", None)
@@ -1302,6 +1358,12 @@ class H(http.server.BaseHTTPRequestHandler):
if ozet_tetik:
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
if via_router and CFG.get('resim_sadelestir', True):
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
# Duzlestirmeden ONCE: kimliksiz arac ciktisi orada duz metne cevrilir ve icindeki resim taninmaz olur.
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
if cikan:
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
@@ -1310,11 +1372,6 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)")
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
fix_tool_ids(req)
if via_router and CFG.get('resim_sadelestir', True):
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
if cikan:
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
merged = coalesce_context(req)
if merged:
@@ -1332,9 +1389,10 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
pencere_biliniyor = "varsayilan" not in ((IDE_BILGI.get(slug) or {}).get("kaynak") or "varsayilan")
# Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir).
pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere)
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target))
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude"))))
if rewriter is not None:
rewriter.dolu = tahmin >= hedef_pencere * 0.7
if kirpilan:
@@ -1359,8 +1417,8 @@ class H(http.server.BaseHTTPRequestHandler):
try:
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
except urllib.error.HTTPError as e:
errbody = e.read()[:400]
last = (e.code, errbody)
errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir
last = (e.code, errbody[:400])
etxt = errbody.decode("utf-8", "replace")
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
@@ -1381,6 +1439,9 @@ class H(http.server.BaseHTTPRequestHandler):
return self._send_sse_failed("context_length_exceeded", etxt[:300])
self.send_response(e.code)
self.send_header("Content-Type", "application/json")
bekle = e.headers.get("Retry-After") if e.headers is not None else None
if bekle:
self.send_header("Retry-After", str(bekle)) # 429'da ne kadar beklenecegi istemciye ulassin
self.send_header("Content-Length", str(len(errbody)))
self.end_headers(); self.wfile.write(errbody); return
except Exception as e:
@@ -1388,6 +1449,9 @@ class H(http.server.BaseHTTPRequestHandler):
if combo and is_timeout(e):
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
break
if combo and kopma_mi(e):
log(f"{tag} combo istegi gonderildikten sonra baglanti koptu, tekrar gonderilmiyor (calisiyor olabilir): {e}")
break
log(f"{tag} deneme {attempt}: {e}")
time.sleep(min(2 * attempt, 8)); continue
# Headers go to the client from here on: a cut stream is never resent.