bridge.py is now a small splitter with no features: a request for one of Codex's own models goes to chatgpt.com byte for byte, a request for an added model goes to kopru.py, which runs as its own process. Codex's own models no longer pass through the feature code at all, and restarting or losing the bridge process does not touch them. Measured with real Codex on spare ports: own model answers; forced compaction on the own model completes (twice) while the bridge process is restarted; added model answers; no own-model request reaches the bridge process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
386 lines
16 KiB
Python
386 lines
16 KiB
Python
"""AITURK IDE "Codex Baglama" ayiricisi.
|
||
|
||
Kullanici karari (11 Ekim 2026): "Codex'in modelleri bir tarafta. Bizim sonradan eklediklerimiz diger tarafta.
|
||
Bizim sonradan eklediklerimiz kopru kullaniyor. Codex'in modelleri kopru kullanmiyor." ve "Codex'i secip Codex'e
|
||
yaziyorsan bizim kopru yardim amacli bile yaninda olamaz."
|
||
|
||
Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya gelir. Bu dosya YALNIZ ayirir:
|
||
|
||
* Codex'in kendi modeli -> chatgpt.com'a; govde bayt bayt ayni gider, cevap geldigi gibi doner.
|
||
* Bizim ekledigimiz model -> ayri bir surecte calisan kopruye (kopru.py). Codex'in kendi modelinin istegi
|
||
(ide/..., 9r/..., model_map) o surece HIC gitmez.
|
||
* Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi.
|
||
* Geri kalan her sey -> chatgpt.com'a, oldugu gibi.
|
||
|
||
BU DOSYAYA OZELLIK EKLENMEZ. Sikistirma, not, talimat, model komutu, gecmis duzeltme: hepsi kopru.py'dedir ve yalniz
|
||
ek modeller icindir. Kopru cokerse, guncellenirse ya da yeniden baslatilirsa Codex'in kendi modelleri etkilenmez.
|
||
|
||
Veri klasoru (bridge.json, log/): --home <klasor> ya da AITURK_CODEX_BRIDGE_HOME. Ajan klasoru: --hermes-home.
|
||
Yerel denetim: GET /aiturk/codex-baglama/status, POST .../stop, POST .../refresh, POST .../kopru-yenile.
|
||
"""
|
||
import gzip
|
||
import hashlib
|
||
import http.client
|
||
import http.server
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import threading
|
||
import time
|
||
import urllib.error
|
||
import urllib.request
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
SURUM = "8" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||
try:
|
||
with open(os.path.abspath(__file__), "rb") as _f:
|
||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
|
||
except OSError:
|
||
KOD = ""
|
||
|
||
|
||
def _arg(name, env):
|
||
"""--name <deger> komut satirindan, yoksa ortam degiskeninden."""
|
||
if name in sys.argv[1:]:
|
||
k = sys.argv.index(name)
|
||
if k + 1 < len(sys.argv):
|
||
return sys.argv[k + 1]
|
||
return os.environ.get(env) or ""
|
||
|
||
|
||
HOME = _arg("--home", "AITURK_CODEX_BRIDGE_HOME") or HERE
|
||
HERMES = _arg("--hermes-home", "HERMES_HOME")
|
||
try:
|
||
with open(os.path.join(HOME, "bridge.json"), encoding="utf-8") as _f:
|
||
CFG = json.load(_f)
|
||
except (OSError, ValueError):
|
||
CFG = {}
|
||
PORT = int(CFG.get("port", 8790))
|
||
KOPRU_PORT = int(_arg("--kopru-port", "AITURK_CODEX_KOPRU_PORT") or PORT + 1)
|
||
CHATGPT = CFG.get("chatgpt_backend", "https://chatgpt.com").rstrip("/")
|
||
KOPRU = f"http://127.0.0.1:{KOPRU_PORT}"
|
||
BEKLEME = float(CFG.get("upstream_idle_timeout", 3600)) # okuma basina; uzun cevap kesilmez (28 Eylul 2026 karari)
|
||
LOGDIR = os.path.join(HOME, "log")
|
||
|
||
# Codex istek govdesini zstd ile sikistirir. Model adini okumak icin bir KOPYASI acilir; giden govde degismez.
|
||
_VENDOR = os.path.join(HERE, "vendor", "cp%d%d-win_amd64" % sys.version_info[:2])
|
||
if os.path.isdir(_VENDOR):
|
||
sys.path.insert(0, _VENDOR)
|
||
|
||
|
||
def _zstd_ac(govde):
|
||
try:
|
||
from compression import zstd as _z
|
||
return _z.decompress(govde)
|
||
except ImportError:
|
||
import zstandard
|
||
return zstandard.ZstdDecompressor().decompressobj().decompress(govde)
|
||
|
||
|
||
def _zstd_hazir():
|
||
try:
|
||
try:
|
||
from compression import zstd as _z
|
||
ornek = _z.compress(b"aiturk")
|
||
except ImportError:
|
||
import zstandard
|
||
ornek = zstandard.ZstdCompressor().compress(b"aiturk")
|
||
return _zstd_ac(ornek) == b"aiturk"
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
# zstd yoksa model adi okunamaz ve ek modeller ayrilamaz; durum ucu ok=false der, IDE Codex'i buraya yoneltmez.
|
||
ZSTD_OK = _zstd_hazir()
|
||
|
||
kilit = threading.Lock()
|
||
sira = [0]
|
||
|
||
|
||
def log(msg):
|
||
satir = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
|
||
try:
|
||
os.makedirs(LOGDIR, exist_ok=True)
|
||
with kilit:
|
||
with open(os.path.join(LOGDIR, "ayirici.log"), "a", encoding="utf-8") as f:
|
||
f.write(satir + "\n")
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def ek_model_mu(slug):
|
||
"""Bizim ekledigimiz model mi? Degilse Codex'in kendi modelidir."""
|
||
slug = str(slug or "")
|
||
onek = (CFG.get("nine_router_catalog") or {}).get("prefix", "9r/")
|
||
return bool(slug) and (slug.startswith("ide/") or bool(onek and slug.startswith(onek))
|
||
or slug in (CFG.get("model_map") or {}))
|
||
|
||
|
||
def model_oku(govde, kodlama):
|
||
"""Istekteki model adi; okunamazsa None (o zaman istek Codex'in kendi yoluna gider). Govdeye dokunmaz."""
|
||
try:
|
||
kodlama = (kodlama or "").strip().lower()
|
||
ham = _zstd_ac(govde) if kodlama == "zstd" else gzip.decompress(govde) if kodlama == "gzip" else govde
|
||
veri = json.loads(ham)
|
||
return veri.get("model") if isinstance(veri, dict) else None
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
ISTEK_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"}
|
||
YANIT_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"}
|
||
|
||
|
||
def kopru_sor(yol, yontem="GET", sure=5.0, govde=None):
|
||
"""Kopru surecinin denetim ucuna sorar; (durum kodu, JSON) ya da ulasilamazsa (None, None)."""
|
||
try:
|
||
istek = urllib.request.Request(KOPRU + yol, data=govde, method=yontem)
|
||
with urllib.request.urlopen(istek, timeout=sure) as yanit:
|
||
return yanit.status, json.loads(yanit.read() or b"{}")
|
||
except Exception:
|
||
return None, None
|
||
|
||
|
||
class Kopru:
|
||
"""Bizim kopruyu (kopru.py) ayri surec olarak calistirir; duserse yeniden baslatir."""
|
||
|
||
def __init__(self):
|
||
self.surec = None
|
||
self.duruyor = False
|
||
self.baslangic = 0.0
|
||
|
||
def komut(self):
|
||
k = [sys.executable, os.path.join(HERE, "kopru.py"), "--home", HOME, "--port", str(KOPRU_PORT)]
|
||
return k + (["--hermes-home", HERMES] if HERMES else [])
|
||
|
||
def baslat(self):
|
||
self.baslangic = time.time()
|
||
self.surec = subprocess.Popen(self.komut(), cwd=HERE, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||
stderr=subprocess.DEVNULL,
|
||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||
log(f"kopru baslatildi (surec {self.surec.pid}, kapi {KOPRU_PORT})")
|
||
|
||
def bekci(self):
|
||
"""Kopru duserse yeniden baslatir: hemen cokuyorsa araligi acar (2, 4, ... en cok 30 sn)."""
|
||
kopru_sor("/aiturk/codex-baglama/stop", "POST", 2.0, b"") # onceki ayiricidan kalmis sahipsiz kopru
|
||
time.sleep(0.5)
|
||
ara = 2.0
|
||
while not self.duruyor:
|
||
try:
|
||
self.baslat()
|
||
kod = self.surec.wait()
|
||
except Exception as e:
|
||
kod = f"{type(e).__name__}: {e}"
|
||
if self.duruyor:
|
||
break
|
||
omur = time.time() - self.baslangic
|
||
ara = 2.0 if omur > 60 else min(ara * 2, 30.0)
|
||
log(f"kopru durdu (cikis {kod}, {int(omur)} sn calisti); {int(ara)} sn sonra yeniden baslatilacak")
|
||
time.sleep(ara)
|
||
|
||
def yenile(self):
|
||
"""Yalniz kopruyu yeniden baslatir; Codex'in kendi modellerinin trafigi kesilmez."""
|
||
if kopru_sor("/aiturk/codex-baglama/stop", "POST", 3.0, b"")[0] is None and self.surec is not None:
|
||
try:
|
||
self.surec.terminate()
|
||
except Exception:
|
||
pass
|
||
|
||
def durdur(self):
|
||
self.duruyor = True
|
||
kopru_sor("/aiturk/codex-baglama/stop", "POST", 3.0, b"")
|
||
try:
|
||
if self.surec is not None:
|
||
self.surec.wait(timeout=3)
|
||
except Exception:
|
||
try:
|
||
self.surec.terminate()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
KOPRU_SUREC = Kopru()
|
||
|
||
|
||
class H(http.server.BaseHTTPRequestHandler):
|
||
protocol_version = "HTTP/1.1"
|
||
|
||
def log_message(self, *a):
|
||
pass
|
||
|
||
def _oku(self):
|
||
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
|
||
out = bytearray()
|
||
while True:
|
||
ham = self.rfile.readline()
|
||
if not ham:
|
||
break
|
||
satir = ham.strip()
|
||
if not satir:
|
||
continue
|
||
boyut = int(satir.split(b";")[0], 16)
|
||
if boyut == 0:
|
||
while self.rfile.readline().strip():
|
||
pass
|
||
break
|
||
out += self.rfile.read(boyut)
|
||
self.rfile.readline()
|
||
return bytes(out)
|
||
n = int(self.headers.get("Content-Length") or 0)
|
||
return self.rfile.read(n) if n else b""
|
||
|
||
def _json(self, durum, veri):
|
||
b = json.dumps(veri).encode()
|
||
self.send_response(durum)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Content-Length", str(len(b)))
|
||
self.end_headers()
|
||
self.wfile.write(b)
|
||
|
||
def _denetim(self, yol, govde):
|
||
son = yol.split("?", 1)[0].rstrip("/")
|
||
if self.command == "POST" and son.endswith("/stop"):
|
||
self._json(200, {"ok": True})
|
||
log("AITURK IDE istegiyle kapatiliyor")
|
||
threading.Thread(target=lambda: (KOPRU_SUREC.durdur(), time.sleep(0.2), os._exit(0)), daemon=True).start()
|
||
return
|
||
if self.command == "POST" and son.endswith("/kopru-yenile"):
|
||
log("kopru yeniden baslatiliyor (istek uzerine); Codex'in kendi modelleri etkilenmez")
|
||
KOPRU_SUREC.yenile()
|
||
return self._json(200, {"ok": True})
|
||
if son.endswith("/status") or (self.command == "POST" and son.endswith("/refresh")):
|
||
kod, veri = kopru_sor(yol, self.command, 40.0 if son.endswith("/refresh") else 5.0,
|
||
govde if self.command == "POST" else None)
|
||
if kod == 200 and isinstance(veri, dict):
|
||
veri.update({"ayirici": True, "ayirici_kod": KOD, "kopru": "calisiyor"})
|
||
veri["ok"] = bool(veri.get("ok")) and ZSTD_OK
|
||
return self._json(200, veri)
|
||
return self._json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": "kapali",
|
||
"ide_model": 0, "ozet": False, "is_hatti": False, "ayirici": True,
|
||
"ayirici_kod": KOD, "kopru": "kapali"})
|
||
return self._json(404, {"error": {"message": "unknown control path"}})
|
||
|
||
def _ilet(self, n, hedef, yol, govde, etiket, yedekli=False, sessiz=False):
|
||
"""Istegi hedefe OLDUGU GIBI iletir, cevabi geldigi gibi (akarak) geri verir.
|
||
yedekli=True: cevap basliklari gelmeden bir sorun olursa hicbir sey gonderilmez ve False doner (cagiran baska
|
||
hedefi dener). Diger durumlarda True doner."""
|
||
tag = f"[{n} {etiket} {self.command} {yol[:70]}]"
|
||
basliklar = {k: v for k, v in self.headers.items() if k.lower() not in ISTEK_ATLA}
|
||
if self.command == "GET" and "/models" in yol:
|
||
# Liste her seferinde tam gelsin: istemcinin elindeki onbellek damgasi iki tarafin listesini karistirir.
|
||
basliklar = {k: v for k, v in basliklar.items() if k.lower() not in ("if-none-match", "if-modified-since")}
|
||
istek = urllib.request.Request(hedef + yol, data=govde or None, headers=basliklar, method=self.command)
|
||
try:
|
||
yanit = urllib.request.urlopen(istek, timeout=20.0 if yedekli else BEKLEME)
|
||
durum = yanit.status
|
||
except urllib.error.HTTPError as e:
|
||
yanit, durum = e, e.code
|
||
except Exception as e:
|
||
if yedekli:
|
||
return False
|
||
log(f"{tag} HATA {type(e).__name__}: {e}")
|
||
if hedef == KOPRU:
|
||
self._json(503, {"error": {"message": "AITURK köprüsü şu an çalışmıyor; birkaç saniye sonra yeniden deneyin "
|
||
"ya da ekrandaki seçiciden Codex'in kendi modelini seçin."}})
|
||
else:
|
||
self._json(502, {"error": {"message": f"baglanti kurulamadi: {e}"}})
|
||
return True
|
||
if yedekli and durum != 200:
|
||
try:
|
||
yanit.read()
|
||
except Exception:
|
||
pass
|
||
return False
|
||
if durum >= 400:
|
||
hata = yanit.read()
|
||
log(f"{tag} {durum}: {hata[:300]!r}")
|
||
self.send_response(durum)
|
||
for k, v in yanit.headers.items():
|
||
if k.lower() not in YANIT_ATLA:
|
||
self.send_header(k, v)
|
||
self.send_header("Content-Length", str(len(hata)))
|
||
self.end_headers()
|
||
self.wfile.write(hata)
|
||
return True
|
||
self.send_response(durum)
|
||
for k, v in yanit.headers.items():
|
||
if k.lower() not in YANIT_ATLA:
|
||
self.send_header(k, v)
|
||
self.send_header("Transfer-Encoding", "chunked")
|
||
self.end_headers()
|
||
toplam = 0
|
||
try:
|
||
while True:
|
||
try:
|
||
parca = yanit.read1(4096) # gelen kadarini hemen verir; bekletmez
|
||
except (OSError, http.client.HTTPException) as e:
|
||
# Hedef koptu: baglanti kapatilir ki Codex yarim akisi tam sanmasin.
|
||
log(f"{tag} hedef koptu ({toplam} bayt): {type(e).__name__}: {e}")
|
||
self.close_connection = True
|
||
return True
|
||
if not parca:
|
||
break
|
||
toplam += len(parca)
|
||
self.wfile.write(b"%x\r\n" % len(parca) + parca + b"\r\n")
|
||
self.wfile.flush()
|
||
self.wfile.write(b"0\r\n\r\n")
|
||
self.wfile.flush()
|
||
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
|
||
log(f"{tag} istemci baglantiyi kapatti ({toplam} bayt)")
|
||
self.close_connection = True
|
||
return True
|
||
if not sessiz:
|
||
log(f"{tag} bitti status={durum} {toplam} bayt")
|
||
return True
|
||
|
||
def _isle(self):
|
||
with kilit:
|
||
sira[0] += 1
|
||
n = sira[0]
|
||
if "websocket" in (self.headers.get("Upgrade") or "").lower():
|
||
# Bugunku calisan durum: surekli acik baglanti reddedilir, Codex olagan yola duser.
|
||
self.send_response(426)
|
||
self.send_header("Content-Length", "0")
|
||
self.send_header("Connection", "close")
|
||
self.end_headers()
|
||
return
|
||
govde = self._oku()
|
||
yol = self.path
|
||
if yol.startswith("/aiturk/codex-baglama/"):
|
||
return self._denetim(yol, govde)
|
||
son = yol.split("?", 1)[0].rstrip("/")
|
||
if self.command == "POST" and son.endswith("/responses"):
|
||
model = model_oku(govde, self.headers.get("Content-Encoding"))
|
||
if ek_model_mu(model):
|
||
return self._ilet(n, KOPRU, yol, govde, f"KOPRU {model}", sessiz=True)
|
||
return self._ilet(n, CHATGPT, yol, govde, f"OZGUN {model or '?'}")
|
||
if self.command == "GET" and "/models" in yol:
|
||
# Ek modeller listede gorunsun diye liste koprüden istenir; kopru cevap vermezse chatgpt.com'un listesi.
|
||
if self._ilet(n, KOPRU, yol, govde, "LISTE kopru", yedekli=True, sessiz=True):
|
||
return
|
||
return self._ilet(n, CHATGPT, yol, govde, "LISTE ozgun", sessiz=True)
|
||
return self._ilet(n, CHATGPT, yol, govde, "OZGUN diger", sessiz=True)
|
||
|
||
do_POST = _isle
|
||
do_GET = _isle
|
||
do_PUT = _isle
|
||
do_DELETE = _isle
|
||
do_PATCH = _isle
|
||
|
||
|
||
class Sunucu(http.server.ThreadingHTTPServer):
|
||
daemon_threads = True
|
||
allow_reuse_address = False # kapi doluysa sessizce yan yana dinlemek yerine acikca hata
|
||
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
sunucu = Sunucu(("127.0.0.1", PORT), H)
|
||
except OSError as e:
|
||
log(f"HATA: 127.0.0.1:{PORT} acilamadi ({e}); baska bir surec bu kapiyi kullaniyor olabilir.")
|
||
sys.exit(1)
|
||
log(f"ayirici :{PORT} Codex'in kendi modelleri -> {CHATGPT} ek modeller -> kopru :{KOPRU_PORT} (kod {KOD})")
|
||
threading.Thread(target=KOPRU_SUREC.bekci, daemon=True).start()
|
||
sunucu.serve_forever()
|