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.name == "nt" and 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()
|