Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/bridge.py
T
yilsemandClaude Opus 5.5 f1802299f0
Server CI and Windows package / windows-package (push) Successful in 12m27s
Server CI and Windows package / verify (push) Successful in 4m37s
fix(codex-link): stop list re-downloads and count context by measurement
Keep the upstream model-list ETag: chatgpt.com repeats it in every answer
(x-models-etag), and the bridge's own stamp made Codex re-download the
~17 MB merged list after every answer (6198 times in one day).

Replace the flat 2 bytes/token estimate with measured ratios (673 real
requests): Claude family and unknown names 2.5, recognised non-Claude
families 3.4. Non-Claude chats were counted about twice as large, were
summarized too early and got false "does not fit" errors.

Ship IDE beta.47 (bridge version 26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 23:40:43 +03:00

829 lines
36 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.
* Surekli baglanti (WebSocket) -> Codex'in kendi modeliyse chatgpt.com'a bayt bayt tunel (Codex'in ozgun hizli yolu).
Ek model ya da gecis gecmisi gorulurse o sohbet 426 ile HTTP'ye birakilir; ek model/gecis mantigi HTTP'de kalir.
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 socket
import ssl
import struct
import urllib.parse
import zlib
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
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "26" # 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
# --- Surekli baglanti (WebSocket) ---------------------------------------------------------------------------
# Kullanici karari (20 Ekim 2026): "Codex orjinal aynen kalacakti, sadece ek combo ve model ekleyecektik."
# Codex abonelik modelinde chatgpt.com'a WebSocket acar; reddedilirse HTTP'ye duser ve her adimda butun sohbeti
# yeniden gonderir. Abonelik sohbeti bu yuzden chatgpt.com'a dokunulmadan tunellenir. Ek model ya da gecis
# gecmisi olan sohbet ilk anda 426 alir ve HTTP'de kalir (bugunku ek model/gecis davranisi aynen).
WS_HTTP = {} # session-id -> zaman: bu Codex surecinde HTTP'de kalacak sohbetler
ws_kilit = threading.Lock()
def ws_ipucu_model(baslik):
"""Codex'in el sikismadaki model ipucu (x-codex-routing-hint: model=gpt-6.1-sol)."""
for parca in (baslik or "").replace(",", ";").split(";"):
k, _, v = parca.partition("=")
if k.strip().lower() == "model":
return v.strip()
return ""
def ws_http_isaretle(sid):
if not sid:
return
with ws_kilit:
WS_HTTP[sid] = time.time()
while len(WS_HTTP) > 4096:
del WS_HTTP[next(iter(WS_HTTP))]
def ws_cerceve_oku(oku):
"""Tek cerceve: (ham_bayt, fin, rsv1, opcode, acik_yuk) ya da baglanti bittiyse None."""
bas = oku(2)
if len(bas) < 2:
return None
b0, b1 = bas[0], bas[1]
n, ek = b1 & 0x7F, b""
if n == 126:
ek = oku(2)
if len(ek) < 2:
return None
n = struct.unpack(">H", ek)[0]
elif n == 127:
ek = oku(8)
if len(ek) < 8:
return None
n = struct.unpack(">Q", ek)[0]
maske = oku(4) if b1 & 0x80 else b""
if b1 & 0x80 and len(maske) < 4:
return None
yuk = oku(n) if n else b""
if len(yuk) < n:
return None
ham = bas + ek + maske + yuk
if maske and n:
m = (maske * (n // 4 + 1))[:n]
yuk = (int.from_bytes(yuk, "big") ^ int.from_bytes(m, "big")).to_bytes(n, "big")
return ham, bool(b0 & 0x80), bool(b0 & 0x40), b0 & 0x0F, yuk
def ws_kapat_cercevesi(kod=1011, neden="AITURK: HTTP yoluna geciliyor"):
yuk = struct.pack(">H", kod) + neden.encode("utf-8")[:120]
return bytes([0x88, len(yuk)]) + yuk
def ws_mesaj_engeli(veri, hesap=""):
"""Bu mesaj chatgpt.com'a tunelden gidemez mi? Neden metni ya da None (gidebilir)."""
try:
m = json.loads(veri)
except (ValueError, UnicodeDecodeError):
return None
if not isinstance(m, dict) or m.get("type") != "response.create":
return None
if ek_model_mu(m.get("model")):
return "ek model"
if gecis_context.foreign(m) or gecis_context.foreign_history(m):
return "gecis gecmisi"
thread = str(m.get("prompt_cache_key") or "")
if thread:
scope = hashlib.sha256((str(hesap or "") + "|" + thread).encode()).hexdigest()
if ek_model_mu(TRANSFERS.get(scope) or ""):
return "gecis gecmisi"
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
observer = None
first_chunks = []
failure_key = getattr(self, "_failure_key", None)
if (hedef == KOPRU and failure_key is not None and durum == 200
and "text/event-stream" in yanit.headers.get("Content-Type", "")
and yanit.headers.get("Content-Encoding", "identity") == "identity"):
observer = terminal_stream.Observer()
if not started:
# Inspect only until the first SSE frame/comment. Keep normal
# streaming and heartbeats; an error-only response can still
# return a truthful HTTP failure before headers are committed.
try:
while True:
first = yanit.read1(4096)
if not first:
break
first_chunks.append(first)
observer.feed(first)
if observer.error and not observer.payload_seen:
yanit.close()
self._json(400, terminal_stream.error_body(observer.error))
return True
if observer.comment_seen or observer.payload_seen or sum(map(len, first_chunks)) >= observer.LIMIT:
break
except (OSError, http.client.HTTPException):
# A transport interruption without an explicit provider
# failure keeps the client's existing recovery behavior.
yanit.close()
self.close_connection = True
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
event_tail = b""
completed = False
try:
for first in first_chunks:
toplam += len(first)
self.wfile.write(b"%x\r\n" % len(first) + first + b"\r\n")
self.wfile.flush()
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)
if observer is not None:
observer.feed(parca)
if getattr(self, "_native_return_scope", None):
event_tail += parca
while b"\n" in event_tail:
line, event_tail = event_tail.split(b"\n", 1)
if line.startswith(b"data:"):
try:
event = json.loads(line[5:])
completed = completed or event.get("type") == "response.completed"
except (ValueError, AttributeError):
pass
self.wfile.write(b"%x\r\n" % len(parca) + parca + b"\r\n")
self.wfile.flush()
if observer is not None and observer.error and not observer.completed:
self.server.failure_replays.remember(failure_key, observer.error)
log(f"{tag} terminal worker error; identical automatic replay will return HTTP 400")
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")
if completed and getattr(self, "_native_return_scope", None):
remember_transfer(*self._native_return_scope)
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 _ws_ret(self, n, neden):
"""426: Codex bekletmeden HTTP yoluna duser (ek model ve gecisler bu yolda islenir)."""
log(f"[{n} WS] HTTP yoluna birakildi: {neden}")
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
self.close_connection = True
def _ws(self, n):
model = ws_ipucu_model(self.headers.get("x-codex-routing-hint"))
sid = self.headers.get("session-id") or self.headers.get("thread-id") or ""
hesap = self.headers.get("chatgpt-account-id") or ""
thread = self.headers.get("thread-id") or sid
scope = hashlib.sha256((str(hesap) + "|" + thread).encode()).hexdigest() if thread else ""
if CFG.get("abonelik_websocket") is False:
return self._ws_ret(n, "ayarla kapali")
if not self.path.split("?", 1)[0].rstrip("/").endswith("/responses"):
return self._ws_ret(n, "yol")
if not model:
return self._ws_ret(n, "model ipucu yok")
if ek_model_mu(model):
return self._ws_ret(n, f"ek model {model}")
with ws_kilit:
http_de = sid in WS_HTTP
if http_de:
return self._ws_ret(n, "sohbet HTTP yolunda")
if scope and ek_model_mu(TRANSFERS.get(scope) or ""):
return self._ws_ret(n, "gecis gecmisi")
u = urllib.parse.urlsplit(CHATGPT)
guvenli = u.scheme == "https"
try:
ust = socket.create_connection((u.hostname, u.port or (443 if guvenli else 80)), timeout=20)
if guvenli:
ust = ssl.create_default_context().wrap_socket(ust, server_hostname=u.hostname)
satirlar = [f"GET {self.path} HTTP/1.1", f"Host: {u.netloc}"]
satirlar += [f"{k}: {v}" for k, v in self.headers.items() if k.lower() != "host"]
ust.sendall(("\r\n".join(satirlar) + "\r\n\r\n").encode("latin-1"))
cevap = b""
while b"\r\n\r\n" not in cevap:
parca = ust.recv(65536)
if not parca or len(cevap) > 65536:
raise OSError("el sikisma cevabi eksik")
cevap += parca
bas, _, artik = cevap.partition(b"\r\n\r\n")
durum = int(bas.split(b" ", 2)[1])
except (OSError, ValueError, IndexError) as e:
return self._ws_ret(n, f"chatgpt.com acilamadi: {type(e).__name__}: {e}")
if durum != 101:
ust.close()
return self._ws_ret(n, f"chatgpt.com {durum}")
uzanti = ""
for satir in bas.split(b"\r\n")[1:]:
k, _, v = satir.decode("latin-1").partition(":")
if k.strip().lower() == "sec-websocket-extensions":
uzanti = v.strip().lower()
sikistirma = "permessage-deflate" in uzanti
baglamsiz = "client_no_context_takeover" in uzanti
self.close_connection = True
yaz_kilit = threading.Lock()
bitti = threading.Event()
ust.settimeout(None)
self.connection.settimeout(None)
def istemciye(veri):
with yaz_kilit:
self.wfile.write(veri)
self.wfile.flush()
def kapat():
bitti.set()
for soket in (ust, self.connection):
try:
soket.shutdown(socket.SHUT_RDWR)
except OSError:
pass
istemciye(bas + b"\r\n\r\n" + artik)
log(f"[{n} WS {model}] chatgpt.com'a dogrudan tunel acildi")
def asagi():
try:
while True:
parca = ust.recv(65536)
if not parca:
break
istemciye(parca)
except OSError:
pass
finally:
kapat()
threading.Thread(target=asagi, daemon=True).start()
acici = zlib.decompressobj(-zlib.MAX_WBITS) if sikistirma else None
ham_parcalar, yukler, sikisik, mesaj = [], [], False, 0
try:
while not bitti.is_set():
c = ws_cerceve_oku(self.rfile.read)
if c is None:
break
ham, fin, rsv1, op, yuk = c
if op >= 8:
ust.sendall(ham)
continue
if op in (1, 2):
ham_parcalar, yukler, sikisik = [ham], [yuk], rsv1
else:
ham_parcalar.append(ham)
yukler.append(yuk)
if not fin:
continue
veri = b"".join(yukler)
if sikisik and acici is not None:
if baglamsiz:
acici = zlib.decompressobj(-zlib.MAX_WBITS)
veri = acici.decompress(veri + b"\x00\x00\xff\xff")
neden = ws_mesaj_engeli(veri, hesap)
if neden:
ws_http_isaretle(sid)
log(f"[{n} WS {model}] {neden}: sohbet HTTP yoluna aktarildi ({mesaj} mesaj tunelden gitti)")
istemciye(ws_kapat_cercevesi())
break
ust.sendall(b"".join(ham_parcalar))
ham_parcalar, yukler = [], []
mesaj += 1
except (OSError, zlib.error) as e:
log(f"[{n} WS {model}] baglanti hatasi: {type(e).__name__}: {e}")
finally:
kapat()
ust.close()
log(f"[{n} WS {model}] kapandi, {mesaj} mesaj")
def _isle(self):
self._native_return_scope = None
self._failure_key = None
with kilit:
sira[0] += 1
n = sira[0]
if "websocket" in (self.headers.get("Upgrade") or "").lower():
return self._ws(n)
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 = {}
if external:
self._failure_key = terminal_stream.request_key(req or {"raw": govde.hex()}, self.headers)
previous_failure = self.server.failure_replays.take(self._failure_key)
if previous_failure:
log(f"[{n} {model}] terminal worker error replay stopped before upstream request")
return self._json(400, terminal_stream.error_body(previous_failure))
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
# A completed return to the subscription ends migration. Historical
# foreign IDs must not keep later native turns in the preparer.
mixed = ek_model_mu(source or "") or (source is None and gecis_context.foreign_history(req))
# Format repair is stateless: the saved transcript can still contain
# foreign wrappers after a successful native return (or a restart).
if external or mixed or gecis_context.foreign(req):
try:
normalized = gecis_context.normalize(req)
if external:
normalized = gecis_context.portable_reasoning(normalized)
except gecis_context.TransferError as e:
self._transfer_begin()
return self._transfer_fail(str(e))
# Small returning requests need no summarizer or local worker.
# Native opaque state must remain on its owner route. Its byte
# size is not a semantic token budget and cannot trigger a
# foreign summarizer, including when transfer state is cold.
if external or (mixed and not gecis_context.native_opaque(normalized)
and gecis_context.tokens(normalized) > int(32768 * 0.70)):
prepared, started = self._prepare(normalized, source)
if prepared is None:
return
if "compaction" in prepared or "compaction_items" in prepared:
if not started:
self._transfer_begin()
items = prepared.get("compaction_items") or [prepared["compaction"]]
events = [{"type": "response.output_item.done", "output_index": index, "item": item} for index, item in enumerate(items)]
events.append({"type": "response.completed", "response": {"id": "resp_aiturk_compact", "object": "response",
"status": "completed", "output": items}})
for event in events:
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
if external and thread:
remember_transfer(scope, model)
self._native_return_scope = (scope, model) if thread and not external else None
return self._ilet(n, KOPRU if external else CHATGPT, yol, body,
f"{'KOPRU' if external else 'OZGUN'} {model}", rewritten=changed, started=started)
self._native_return_scope = (scope, model) if thread else None
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):
def __init__(self, *args, **kwargs):
self.failure_replays = terminal_stream.FailureReplays()
super().__init__(*args, **kwargs)
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()