fix(codex-link): isolate model transfers and widen local connection capacity
This commit is contained in:
1 parent
2718b460ed
commit
369f6c2ca8
16 files changed
+790
-40
No files matched your search
@@ -1,8 +1,8 @@
|
||||
"""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."
|
||||
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:
|
||||
|
||||
@@ -12,8 +12,9 @@ Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya g
|
||||
* 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.
|
||||
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.
|
||||
@@ -30,9 +31,10 @@ import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import gecis_context
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "10" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
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]
|
||||
@@ -95,14 +97,43 @@ def _zstd_hazir():
|
||||
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 kilit:
|
||||
with log_kilit:
|
||||
with open(os.path.join(LOGDIR, "ayirici.log"), "a", encoding="utf-8") as f:
|
||||
f.write(satir + "\n")
|
||||
except OSError:
|
||||
@@ -254,6 +285,8 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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",
|
||||
@@ -261,12 +294,14 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
"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):
|
||||
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")}
|
||||
@@ -280,6 +315,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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."}})
|
||||
@@ -295,6 +333,13 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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:
|
||||
@@ -303,12 +348,13 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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()
|
||||
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:
|
||||
@@ -334,6 +380,63 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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
|
||||
@@ -352,8 +455,50 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
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)
|
||||
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.
|
||||
@@ -371,6 +516,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user