Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/bridge.py
T
yilsem 369f6c2ca8
Server CI and Windows package / verify (push) Successful in 2m3s
Server CI and Windows package / windows-package (push) Successful in 6m58s
fix(codex-link): isolate model transfers and widen local connection capacity
2026-10-13 00:35:08 +03:00

534 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""AITURK IDE "Codex Baglama" ayiricisi.
13 Ekim 2026 aciklamasi: abonelik ve ek modellerin saglayici yollari ayri
kalir; gecis gecmisinin hazirlanmasi yasak degildir. Ses Codex'in yerlisik
ses hattinda kalir. Saf abonelik sohbeti degistirilmez.
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.
Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik
inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider.
Saf abonelik sohbetleri ve ses, ozet servisinin durumundan 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
import gecis_context
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "11" # 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()
log_kilit = threading.Lock()
transfer_kilit = threading.Lock()
sira = [0]
# Store only hashed conversation keys and the last external model, not messages
# or credentials. Survives restarts so returning long chats keep their guard.
TRANSFER_PATH = os.path.join(HOME, "gecis-sohbetler.json")
try:
with open(TRANSFER_PATH, encoding="utf-8") as f:
TRANSFERS = json.load(f)
if not isinstance(TRANSFERS, dict):
TRANSFERS = {}
except (OSError, ValueError):
TRANSFERS = {}
def remember_transfer(scope, model):
with transfer_kilit:
if TRANSFERS.get(scope) == model:
return
TRANSFERS[scope] = model
while len(TRANSFERS) > 2048:
del TRANSFERS[next(iter(TRANSFERS))]
try:
os.makedirs(HOME, exist_ok=True)
with open(TRANSFER_PATH + ".tmp", "w", encoding="utf-8") as f:
json.dump(TRANSFERS, f)
os.replace(TRANSFER_PATH + ".tmp", TRANSFER_PATH)
except OSError:
pass # in-memory guard remains active
def log(msg):
satir = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
try:
os.makedirs(LOGDIR, exist_ok=True)
with log_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"})
# IDE compares the splitter, not the older worker's version.
veri["surum"] = SURUM
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, rewritten=False, started=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 rewritten:
basliklar = {k: v for k, v in basliklar.items() if k.lower() != "content-encoding"}
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 started:
self._transfer_fail("Model bağlantısı kurulamadı; sohbet geçmişi korundu.")
return True
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}")
if started:
try:
message = json.loads(hata).get("error", {}).get("message", "Model isteği reddedildi.")
except (ValueError, AttributeError):
message = "Model isteği reddedildi (HTTP %d)." % durum
self._transfer_fail(message)
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("Content-Length", str(len(hata)))
self.end_headers()
self.wfile.write(hata)
return True
if not started:
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 _transfer_begin(self):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
def _transfer_chunk(self, body):
self.wfile.write(b"%x\r\n" % len(body) + body + b"\r\n")
self.wfile.flush()
def _transfer_fail(self, message):
event = {"type": "response.failed", "response": {"id": "resp_aiturk_transfer", "status": "failed",
"output": [], "error": {"code": "aiturk_transfer_failed", "message": message}}}
self._transfer_chunk(b"event: response.failed\ndata: " + json.dumps(event, ensure_ascii=False).encode() + b"\n\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
def _prepare(self, req, source_model):
"""Preparation is separate from inference. ChatGPT auth never goes to AITURK upstream.
The local worker needs subscription auth only to render opaque OpenAI
summaries through their owner. It returns history, never a task answer.
"""
done, result = threading.Event(), {}
def run():
try:
headers = {"Content-Type": "application/json", "X-AITURK-Transfer": "1"}
for key in ("Authorization", "chatgpt-account-id", "originator"):
if self.headers.get(key):
headers[key] = self.headers[key]
data = json.dumps({"request": req, "source_model": source_model}, ensure_ascii=False).encode()
request = urllib.request.Request(KOPRU + "/aiturk/codex-baglama/prepare-transfer", data=data, headers=headers)
with urllib.request.urlopen(request, timeout=BEKLEME) as response:
result["value"] = json.load(response)
except urllib.error.HTTPError as e:
try:
result["error"] = json.loads(e.read())["error"]["message"]
except Exception:
result["error"] = "Geçiş hazırlığı başarısız; geçmiş korundu."
except Exception:
result["error"] = "AITURK geçiş hazırlığına ulaşılamadı; geçmiş korundu."
finally:
done.set()
threading.Thread(target=run, daemon=True).start()
started = not done.wait(0.2)
if started:
self._transfer_begin()
self._transfer_chunk(b": AITURK context preparation\n\n")
while not done.wait(10):
self._transfer_chunk(b": AITURK context preparation\n\n")
if "error" in result:
if not started:
self._transfer_begin()
self._transfer_fail(result["error"])
return None, True
return result["value"], started
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"))
external = ek_model_mu(model)
try:
encoding = (self.headers.get("Content-Encoding") or "").lower()
raw = _zstd_ac(govde) if encoding == "zstd" else gzip.decompress(govde) if encoding == "gzip" else govde
req = json.loads(raw)
if not isinstance(req, dict):
req = {}
except Exception:
req = {}
thread = str(req.get("prompt_cache_key") or "")
scope = hashlib.sha256((str(self.headers.get("chatgpt-account-id") or "") + "|" + thread).encode()).hexdigest()
source = TRANSFERS.get(scope) if thread else None
mixed = bool(source) or gecis_context.foreign(req)
if external and thread:
remember_transfer(scope, model)
if external or mixed:
try:
normalized = gecis_context.normalize(req)
except gecis_context.TransferError as e:
self._transfer_begin()
return self._transfer_fail(str(e))
# Small returning requests need no summarizer or local worker.
if external or gecis_context.tokens(normalized) > int(32768 * 0.70):
prepared, started = self._prepare(normalized, source)
if prepared is None:
return
if "compaction" in prepared:
if not started:
self._transfer_begin()
item = prepared["compaction"]
for event in ({"type": "response.output_item.done", "output_index": 0, "item": item},
{"type": "response.completed", "response": {"id": "resp_aiturk_compact", "object": "response",
"status": "completed", "output": [item]}}):
self._transfer_chunk(b"event: " + event["type"].encode() + b"\ndata: " + json.dumps(event, ensure_ascii=False).encode() + b"\n\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
return
normalized = prepared["request"]
else:
started = False
changed = normalized != req
body = json.dumps(normalized, ensure_ascii=False).encode() if changed else govde
return self._ilet(n, KOPRU if external else CHATGPT, yol, body,
f"{'KOPRU' if external else 'OZGUN'} {model}", rewritten=changed, started=started)
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
# Shipped Python 3.11 defaults to only five pending connections. Model
# discovery, voice and simultaneous chats can arrive in the same burst.
request_queue_size = 128
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()