From 23e8aa8191f1ac86f4d564109b72859d5c0726fd Mon Sep 17 00:00:00 2001 From: yilsem Date: Sun, 11 Oct 2026 06:13:29 +0300 Subject: [PATCH] feat(codex-bridge): split into a splitter and a separate bridge process 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 --- apps/desktop/assets/codex-bridge/bridge.py | 1790 +++-------------- apps/desktop/assets/codex-bridge/kopru.py | 1566 ++++++++++++++ .../codex-bridge/test_access_programs.py | 2 +- .../assets/codex-bridge/test_ayirici.py | 214 ++ .../assets/codex-bridge/test_claude_sonu.py | 2 +- .../assets/codex-bridge/test_combo_tekrar.py | 2 +- .../assets/codex-bridge/test_denetim_ek.py | 2 +- .../assets/codex-bridge/test_kopru7_ek.py | 2 +- .../assets/codex-bridge/test_model_switch.py | 2 +- .../assets/codex-bridge/test_pencere_phase.py | 2 +- apps/desktop/assets/codex-bridge/test_zstd.py | 2 +- 11 files changed, 2093 insertions(+), 1493 deletions(-) create mode 100644 apps/desktop/assets/codex-bridge/kopru.py create mode 100644 apps/desktop/assets/codex-bridge/test_ayirici.py diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index 0c9ed3b..c7e77f0 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -1,79 +1,47 @@ -"""AITURK IDE Codex bridge. +"""AITURK IDE "Codex Baglama" ayiricisi. -A small local HTTP proxy between OpenAI Codex (desktop or CLI) and its backend. -AITURK IDE's "Codex Baglama" setting points Codex's `openai_base_url` at it. +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." -- POST .../responses -> by model slug: - * subscription models (no mapping) -> chatgpt.com, unchanged - * "ide/" models -> the AITURK gateway the IDE itself uses -- GET .../models -> the IDE's models and combos are added to Codex's own list -- everything else -> chatgpt.com, unchanged (voice, remote, auth) -- Conversation summaries (compaction) are produced as plain text so that every - provider can read them when the user switches models (ozet_kopru.py). +Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya gelir. Bu dosya YALNIZ ayirir: -Data directory (bridge.json, log/): --home or AITURK_CODEX_BRIDGE_HOME, -otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME. -Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop. + * 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 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 http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib -from gemini_context import coalesce_context -try: - from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context -except ImportError: - def pano_blogunu_ulastir(req): - return 0, "" -from resim_context import resimleri_sadelestir -import ide_catalog -import ozet_kopru -import pencere -import yarim -import model_gecis -import is_hatti +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 -SURUM = "8" 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] # ayni surum numarasi altindaki farkli kodu ayirt eder + KOD = hashlib.sha256(_f.read()).hexdigest()[:8] except OSError: KOD = "" -# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older -# interpreters the matching `zstandard` build ships next to this file. -_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_decompress(body): - try: - from compression import zstd as _z - return _z.decompress(body) - except ImportError: - import zstandard - return zstandard.ZstdDecompressor().decompressobj().decompress(body) - - -def _zstd_ready(): - try: - try: - from compression import zstd as _z - sample = _z.compress(b"aiturk") - except ImportError: - import zstandard - sample = zstandard.ZstdCompressor().compress(b"aiturk") - return _zstd_decompress(sample) == b"aiturk" - except Exception: - return False - - -# Without zstd every Codex request would fail; the status endpoint then reports -# ok=false so that the IDE never points Codex at this bridge. -ZSTD_OK = _zstd_ready() - def _arg(name, env): - """--name from the command line, else the environment variable.""" + """--name komut satirindan, yoksa ortam degiskeninden.""" if name in sys.argv[1:]: k = sys.argv.index(name) if k + 1 < len(sys.argv): @@ -82,1484 +50,336 @@ def _arg(name, env): HOME = _arg("--home", "AITURK_CODEX_BRIDGE_HOME") or HERE -if _arg("--hermes-home", "HERMES_HOME"): - os.environ["HERMES_HOME"] = _arg("--hermes-home", "HERMES_HOME") +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 = {} # no settings file: subscription models pass through, IDE catalog on + 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("/") -ROUTES = dict(CFG.get("routes") or {}) # {"cheapvibecode": {"url","key","flatten_tools"}, ...} -RETRIES = int(CFG.get("retries", 6)) -# Socket idle (per read) timeout toward 9Router/chatgpt.com. 9Router and OmniRoute keep -# long answers alive with a byte every 15 s, so this only fires on a dead peer; 60+ min -# per user decision (28 Sep 2026) so a heavy Fusion is never cut here. -UPSTREAM_IDLE_TIMEOUT = float(CFG.get("upstream_idle_timeout", 3600)) - -def is_timeout(error): - """True for a socket/read timeout, also when urllib wraps it in URLError.""" - reason = getattr(error, "reason", None) - return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError) -def kopma_mi(error): - """Baglanti kurulduktan sonra uzak uc kapatti ya da sifirladi (yanit basliklari gelmeden): istek ulasmis ve - is baslamis olabilir. Baglanti hic kurulamadiysa (reddedildi, DNS) False.""" - reason = getattr(error, "reason", None) - turler = (ConnectionResetError, ConnectionAbortedError, http.client.IncompleteRead) # RemoteDisconnected dahil - return isinstance(error, turler) or isinstance(reason, turler) -DEBUG = bool(CFG.get("debug", False)) -UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)") -# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir. -NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"]) +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") -os.makedirs(LOGDIR, exist_ok=True) -lock = threading.Lock() -seq = [0] + +# 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): - line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg - print(line, flush=True) - with lock: - with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f: - f.write(line + "\n") - -# ---- model yonlendirme tablosu: slug -> (route, hedef model) ---- -MODEL_MAP = {} # slug -> (route, hedef model, flatten_tools or None) -for slug, v in CFG.get("model_map", {}).items(): - if isinstance(v, str): - v = {"route": "cheapvibecode", "model": v} - MODEL_MAP[slug] = (v["route"], v["model"], v.get("flatten_tools")) - -EXTRA_MODELS = list(CFG.get("extra_models", [])) -for m in EXTRA_MODELS: - MODEL_MAP.setdefault(m["slug"], (m.get("route", "cheapvibecode"), m.get("target", m["slug"]), m.get("flatten_tools"))) - -def load_9router_catalog(): - global MODEL_MAP, EXTRA_MODELS - cat = CFG.get("nine_router_catalog") or {} - if not cat.get("enabled"): - return - route = cat.get("route", "9router") - r = ROUTES.get(route) - if not r: - return - prefix = cat.get("prefix", "9r/") + satir = time.strftime("%Y-%m-%d %H:%M:%S ") + msg try: - req = urllib.request.Request(r["url"].rsplit("/responses", 1)[0] + "/models", - headers={"Authorization": f"Bearer {r['key']}", "User-Agent": UA}) - data = json.load(urllib.request.urlopen(req, timeout=30)) - except Exception as e: - log(f"9Router katalogu alinamadi: {e}") - return - model_map = {k: v for k, v in MODEL_MAP.items() if not k.startswith(prefix)} - extra_models = [m for m in EXTRA_MODELS if not m["slug"].startswith(prefix)] - n = 0 - # Secicide yalniz "saglayici/model" gorunur (kullanici istegi, 9 Ekim 2026). - adlar = ide_catalog.adlandir([(m.get("id", ""), m.get("id", "")) for m in data.get("data", []) if m.get("id")]) - for m in data.get("data", []): - mid = m.get("id", "") - slug = prefix + mid - if not mid or slug in model_map: - continue - extra_models.append({"slug": slug, "display_name": adlar.get(mid, mid), - "description": f"9Router: {mid}", "route": route, "target": mid, - "clone_from": cat.get("clone_from", "gpt-6-astra"), - "priority": 100 + n, "reasoning_levels": cat.get("reasoning_levels"), - "context_window": m.get("context_window") or m.get("context_length")}) - model_map[slug] = (route, mid, None) - n += 1 - MODEL_MAP = model_map - EXTRA_MODELS = extra_models - log(f"9Router katalogu: {n} model eklendi") + 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 -load_9router_catalog() - -# ---- AITURK IDE "Codex Baglama": IDE'nin model/combolari (ide_catalog.py, 9 Ekim 2026) ---- -IDE_PREFIX = "ide/" -IDE_ROUTE = "aiturk-ide" -IDE_DURUM = [None] -IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz) - -def load_ide_catalog(ag=True): - """IDE'nin gecit katalogunu "ide/" olarak ekler. Ayar kapaliysa ya da IDE anahtari - yoksa girdiler kaldirilir; gecit gecici olarak cevap vermezse son liste korunur.""" - global MODEL_MAP, EXTRA_MODELS - try: - durum, base, anahtar, girdiler = ide_catalog.yukle(UA, home=HOME, ag=ag) - except Exception as e: - log(f"AITURK IDE katalogu alinamadi, son liste korunuyor: {e}") - return - model_map = {k: v for k, v in MODEL_MAP.items() if not k.startswith(IDE_PREFIX)} - extra_models = [m for m in EXTRA_MODELS if not m["slug"].startswith(IDE_PREFIX)] - n = 0 - if durum == "tamam": - # Anahtar yalniz bellekte tutulur; bridge.json'a yazilmaz. - ROUTES[IDE_ROUTE] = {"url": base + "/responses", "key": anahtar, "flatten_tools": True} - clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra") - levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels") - # Gorunen adlari ayirt et; farkli kimlikteki modeller adlari benziyor diye silinmez. - var_olan = {m.get("display_name", "").lower() for m in extra_models} - adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan, - turler={g["id"]: g["kind"] for g in girdiler}) - for g in girdiler: - slug = IDE_PREFIX + g["id"] - if slug in model_map: - continue - extra_models.append({"slug": slug, "display_name": adlar[g["id"]], - "description": g.get("aciklama") or f"AITURK IDE: {g['ad']}", - "route": IDE_ROUTE, "target": g["id"], - "clone_from": clone, "priority": 300 + n, "reasoning_levels": levels, - "kind": g["kind"], "context_window": g.get("pencere")}) - IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"), - "kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))} - model_map[slug] = (IDE_ROUTE, g["id"], None) - n += 1 - else: - ROUTES.pop(IDE_ROUTE, None) - MODEL_MAP = model_map - EXTRA_MODELS = extra_models - ozet = f"{durum}, {n} model" - if IDE_DURUM[0] != ozet: # 5 dakikada bir ayni satiri yazmamak icin - IDE_DURUM[0] = ozet - log(f"AITURK IDE Codex Baglama: {ozet}") - -# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler); -# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir. -load_ide_catalog(ag=False) - -def catalog_refresh_loop(): - """Kataloglari arka planda yeniler; liste istegini yavaslatmaz.""" - minutes = max(1, int(CFG.get("catalog_refresh_min", 5))) - time.sleep(3) - try: - load_ide_catalog() # acilistan hemen sonra: guncel pencere verisiyle - except Exception as e: - log(f"katalog yenileme hatasi: {e}") - while True: - time.sleep(minutes * 60) - for yukle in (load_9router_catalog, load_ide_catalog): - try: - yukle() - except Exception as e: - log(f"katalog yenileme hatasi: {e}") - -threading.Thread(target=catalog_refresh_loop, daemon=True).start() - -# ---- AITURK is hatti: isi gonderebilen AITURK komutu (is_hatti.py). Kurulum sonradan gelebilir: yoksa 2 dk'da bir bakilir. -HAT_KOMUT = [None] - -def hat_komut_loop(): - while True: - try: - if (CFG.get("aiturk_hat") or {}).get("enabled") is not False: - bulunan = is_hatti.komut_bul() - if bulunan != HAT_KOMUT[0]: - HAT_KOMUT[0] = bulunan - log(f"AITURK is hatti: {'acik, komut ' + bulunan if bulunan else 'kapali (aiturk is komutu yok)'}") - except Exception as e: - log(f"AITURK is hatti komutu aranamadi: {type(e).__name__}: {e}") - time.sleep(600 if HAT_KOMUT[0] else 120) - -threading.Thread(target=hat_komut_loop, daemon=True).start() - -LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir) -CACHE_DIR = os.path.join(LOGDIR, "son-liste") - -def _cache_path(acct): - guvenli = re.sub(r"[^A-Za-z0-9._-]", "_", acct or "-")[:64] - return os.path.join(CACHE_DIR, f"models-{guvenli}.json") - -def save_last_models(acct, hdrs, body): - """Son basarili listeyi diske yazar. Kopru yeniden baslayinca bellek - sifirlandigi icin, restarttan sonraki ilk 401 listeyi silip goturuyordu.""" - try: - os.makedirs(CACHE_DIR, exist_ok=True) - yol = _cache_path(acct) - veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"), - "body": body.decode("utf-8")} - gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # ayni hesabin iki es zamanli kaydi carpismasin - with open(gecici, "w", encoding="utf-8") as f: - json.dump(veri, f, ensure_ascii=False) - os.replace(gecici, yol) # yarim dosya birakmamak icin - except Exception as e: - log(f"son liste diske yazilamadi: {e}") - -def load_last_models(): - """Acilista diskteki son listeleri bellege alir.""" - try: - if not os.path.isdir(CACHE_DIR): - return - for ad in os.listdir(CACHE_DIR): - if not (ad.startswith("models-") and ad.endswith(".json")): - continue - try: - with open(os.path.join(CACHE_DIR, ad), encoding="utf-8") as f: - v = json.load(f) - acct = v.get("acct") or ad[len("models-"):-len(".json")] - LAST_MODELS[acct] = {"headers": [tuple(h) for h in v["headers"]], - "body": v["body"].encode("utf-8"), "at": v.get("at", "?")} - log(f"diskten son liste alindi ({acct}, {len(v['body'])} bayt, {v.get('at')})") - except Exception as e: - log(f"son liste okunamadi {ad}: {e}") - except Exception as e: - log(f"son liste klasoru okunamadi: {e}") -SLIM_CLONES = bool(CFG.get("slim_clones", False)) -SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"]) - -# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ---- -def sema_duzelt(sema): - """Arac parametre semasinin EN UST duzeyindeki oneOf/anyOf/allOf'u tek nesne semasina indirir. - Claude bunu reddediyor ("input_schema does not support oneOf, allOf, or anyOf at the top level"; olcum - 11 Ekim 2026: masaustu Codex'in klasik arac listesindeki bir arac yuzunden her Opus istegi 400 aldi). - Secenek nesnelerinin alanlari birlestirilir; zorunlu alanlar oneOf/anyOf'ta kesisim, allOf'ta birlesimdir. - Donus: (sema, degisti mi).""" - if not isinstance(sema, dict) or not any(k in sema for k in ("oneOf", "anyOf", "allOf")): - return sema, False - yeni = {k: v for k, v in sema.items() if k not in ("oneOf", "anyOf", "allOf")} - alanlar = dict(yeni.get("properties") or {}) - zorunlu = set(yeni.get("required") or []) - for anahtar in ("oneOf", "anyOf", "allOf"): - secenekler = [x for x in (sema.get(anahtar) or []) if isinstance(x, dict)] - kesisim = None - for secenek in secenekler: - secenek, _ = sema_duzelt(secenek) - for ad, tanim in (secenek.get("properties") or {}).items(): - alanlar.setdefault(ad, tanim) - gerekli = set(secenek.get("required") or []) - if anahtar == "allOf": - zorunlu |= gerekli - else: - kesisim = gerekli if kesisim is None else (kesisim & gerekli) - if anahtar != "allOf" and kesisim: - zorunlu |= kesisim - yeni["type"] = "object" - yeni["properties"] = alanlar - if zorunlu: - yeni["required"] = sorted(a for a in zorunlu if a in alanlar) - else: - yeni.pop("required", None) - return yeni, True - -def arac_semalarini_duzelt(req): - """Dis modele giden duz arac listesinde ust duzey birlesimli semalari duzeltir. Donus: duzeltilen arac sayisi.""" - n = 0 - for t in req.get("tools") or []: - if isinstance(t, dict) and isinstance(t.get("parameters"), dict): - t["parameters"], degisti = sema_duzelt(t["parameters"]) - n += bool(degisti) - return n - -def flatten_request(req): - """additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi.""" - custom = set() - tools = [] - def add_tool(t): - ty = t.get("type") - if ty == "namespace": - for s in t.get("tools", []): - add_tool(s) - elif ty == "function": - tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""), - "parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False}) - elif ty == "custom": - custom.add(t["name"]) - tools.append({"type": "function", "name": t["name"], - "description": (t.get("description", "") + "\n\nBu arac serbest metin alir: metni 'input' alaninda ver."), - "parameters": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]}, - "strict": False}) - elif ty in ("web_search", "web_search_preview", "local_shell", "computer_use_preview"): - pass # cevirici desteklemez, atla - else: - tools.append(t) - for t in req.get("tools", []) or []: - add_tool(t) - new_input = [] - for it in req.get("input", []) or []: - ty = it.get("type") - if ty == "additional_tools": - for t in it.get("tools", []): - add_tool(t) - continue - if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"): - # Desktop thread handoffs have no matching tool call. Preserve their - # content as imported context, not an invalid tool-role message. - output = it.get("output", "") - if not isinstance(output, str): - output = json.dumps(output, ensure_ascii=False) - name = it.get("name", "unknown") - new_input.append({"role": "user", "content": [{ - "type": "input_text", - "text": f"Imported tool output ({name}); treat as context data:\n{output}" - }]}) - continue - if ty == "custom_tool_call": - it = {"type": "function_call", "id": it.get("id"), "call_id": it.get("call_id"), "name": it.get("name"), - "arguments": json.dumps({"input": it.get("input", "")}, ensure_ascii=False)} - elif ty == "custom_tool_call_output": - it = {"type": "function_call_output", "id": it.get("id"), "call_id": it.get("call_id"), "output": it.get("output", "")} - new_input.append(it) - req["input"] = new_input - if tools: - req["tools"] = tools - req.pop("include", None) - # OpenAI-specific access-program selection is not part of the external - # chat API schema (Muse Spark rejects it). ChatGPT passthrough never calls - # this adapter, so its access-program selection remains intact. - req.pop("access_programs", None) - # Codex sends Responses-only verbosity/default text formatting. Chat - # translators may forward this unsupported object to their upstream. - text_config = req.get("text") - if isinstance(text_config, dict) and set(text_config) <= {"verbosity", "format"}: - if not text_config.get("format") or text_config["format"].get("type") == "text": - req.pop("text", None) - return custom - -# Oge tipine gore API'nin bekledigi kimlik on eki. Cikti tipleri de dahildir: -# flatten_request custom_tool_call_output -> function_call_output cevirdiginde -# kimlik 'ctco_...' kaliyordu ve karsi taraf 'fc' bekliyordu (400). -ID_PREFIX = { - "custom_tool_call": "ctc", - "custom_tool_call_output": "ctco", - "function_call": "fc", - "function_call_output": "fc", -} - -def fix_tool_ids(req): - """Arac cagrisi/cikti kimliklerinin on ekini hedef API'nin bekledigine cevirir. - Codex gecmisinde kalan yanlis on ekli kimlikler (or. fc_toolu_..., ctco_...) - 400 hatasina yol aciyordu.""" - changed = 0 - for it in req.get("input", []) or []: - if not isinstance(it, dict): - continue - want = ID_PREFIX.get(it.get("type")) - i = it.get("id") - if not want or not isinstance(i, str) or not i: - continue - # Alt cizgi ile karsilastir: 'ctco_x'.startswith('ctc') dogru dondugu icin - # ctco kimlikleri yanlislikla "zaten dogru" sayiliyordu. - if i.startswith(want + "_"): - continue - tail = i.split("_", 1)[1] if "_" in i else i - it["id"] = want + "_" + tail - changed += 1 - return changed - - -# Cevirici uretimi kimliklerin izi. 9Router chat.completions'a cevirdigi icin -# uydurdugu kimliklerde bu parca bulunur; chatgpt.com boyle bir kaydi saklamaz. -FOREIGN_ID_MARK = "chatcmpl-" - -# Icerigini tasimayan, yalnizca hedefteki kayda ISARET EDEN oge tipleri. -# Kimlik yabanciysa hedef cozemez; ceviriyle kurtarilamaz, atilmalari gerekir. -REFERENCE_TYPES = ("reasoning", "item_reference") - -# Icerigini kendisi tasiyan tipler: yabanci kimlik tasisalar da sorun degil, -# uyari uretmezler. ID_PREFIX'teki tipler de bu gruba dahildir. -INLINE_TYPES = ("message",) - -def drop_foreign_items(req): - """Baska bir saglayicinin urettigi, hedefin cozemeyecegi ogeleri atar. - - Ilke hermes-agent'tan: sahipli kimligi tasima. hermes gecmisi saglayicidan - bagimsiz tuttugu icin model degistirince hicbir sey kirilmiyor; Codex ise - Responses bicimindeki sahipli kimlikleri tasiyor. Model degistirildiginde - chatgpt.com 404 'Item ... not found. Items are not persisted when store is - set to false' diyor ve tur hic baslamiyordu. - - Icerigini iceren ogeler (function_call vb.) atilmaz; onlarin kimligi - fix_tool_ids ile duzeltilir. Yalnizca isaret eden tipler atilir.""" - inp = req.get("input") - if not isinstance(inp, list): - return 0 - kalan, atilan, bilinmeyen = [], 0, set() - for it in inp: - i = it.get("id") if isinstance(it, dict) else None - if isinstance(i, str) and FOREIGN_ID_MARK in i: - ty = it.get("type") - if ty in REFERENCE_TYPES: - atilan += 1 - continue - if ty not in ID_PREFIX and ty not in INLINE_TYPES: - # Tanimadigimiz bir tip yabanci kimlik tasiyor. Simdilik - # gecirilir; hata verirse gunlukte hangi tip oldugu yazili olur. - bilinmeyen.add(ty) - kalan.append(it) - if atilan: - req["input"] = kalan - if bilinmeyen: - log(f"UYARI: yabanci kimlikli taninmayan oge tipleri gecirildi: {sorted(bilinmeyen)}") - return atilan - - -ARAC_OGELERI = {"function_call", "custom_tool_call", "local_shell_call", "tool_search_call", "web_search_call", - "mcp_call", "computer_call", "image_generation_call", "code_interpreter_call"} - -def asistan_mesaji_mi(item): - return isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "assistant" - -def phase_damgala(cikti): - """Bitmis yanitin cikti listesinde phase'i olmayan asistan mesajlarini damgalar. Kural: yanitta arac - cagrisi varsa tur suruyor demektir, butun mesajlar ara aciklamadir; yoksa yalniz SON mesaj son cevaptir. - Codex masaustu ara mesaji bu isaretle tanir; isaretsiz her mesaji son cevap sayar (10 Ekim 2026).""" - arac_var = any(isinstance(o, dict) and o.get("type") in ARAC_OGELERI for o in cikti) - mesajlar = [o for o in cikti if asistan_mesaji_mi(o)] - for o in mesajlar: - if not o.get("phase"): - o["phase"] = "commentary" if (arac_var or o is not mesajlar[-1]) else "final_answer" - -class SseRewriter: - """function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE. - Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne - geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti -> - final_answer). Metin parcalari (delta) bekletilmez, canli akar.""" - def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False): - self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor - self.custom = custom - self.buf = b"" - self.custom_ids = set() - self.bekleyen = None # phase karari bekleyen "output_item.done" olayi - self.arac_goruldu = False - self.hedef = hedef # gecit model kimligi (tasma siniri ogrenmek icin) - self.tasma = tasma # callable(hedef, hata metni): sinir ogrenildiyse cagrilir - self.aracsiz = aracsiz # callable(hedef): hedef "arac cagrisi desteklenmiyor" dedi - def _birak(self, phase): - d, self.bekleyen = self.bekleyen, None - if d is not None and not d["item"].get("phase"): - d["item"]["phase"] = phase - return d - def _gorunur_hata(self, mesaj): - """Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026) - hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda - kullanici hatayi duyar.""" - damga = "%d" % int(time.time() * 1000) - rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga - oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer", - "content": [{"type": "output_text", "text": mesaj, "annotations": []}]} - kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0, - "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0} - return [ - {"type": "response.output_item.added", "output_index": 0, - "item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}}, - {"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj}, - {"type": "response.output_item.done", "output_index": 0, "item": oge}, - {"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed", - "output": [oge], "usage": kullanim}}, - ] - - def _sirala(self, d): - """Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir).""" - t = d.get("type", "") - if t == "error" and not isinstance(d.get("error"), dict) and d.get("message"): - # Gecidin duz bicimi: {"type":"error","code":"bad_request","message":"[400]: ...","status_code":400} - d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""), - "status_code": d.get("status_code")}} - if t == "error" and isinstance(d.get("error"), dict): - cevrilen = self._hata_cevir(d) - if cevrilen is not None: - # Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir. - once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else [] - return once + cevrilen - return self._sirala_normal(d, t) - - def _hata_cevir(self, d): - """Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir.""" - # Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz, - # 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in - # tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin. - metin = str(d["error"].get("message") or d["error"].get("type") or "") - if pencere.aracsiz_mi(metin): - if self.aracsiz is not None: - self.aracsiz(self.hedef) - return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.") - if self.dolu and pencere.kapali_hata_mi(metin): - # Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma - # yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil). - return [{"type": "response.failed", "response": { - "id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], - "error": {"code": "context_length_exceeded", "message": - "Geçit isteği reddetti ve nedenini bildirmedi. Sohbet seçili modelin bağlam penceresine çok yakın; " - "sığmamış olabilir. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}] - if pencere.tasma_mi(metin): - if self.tasma is not None: - self.tasma(self.hedef, metin) - return [{"type": "response.failed", "response": { - "id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], - "error": {"code": "context_length_exceeded", "message": metin[:300]}}}] - if re.search(r"(?" olarak. Soylenen ad - Codex listesinde yoksa buraya bakilir (olcum 11 Ekim 2026: "Seed 2.1 Turbo" gecitte vardi, listede yoktu). - 10 dakika bellekte tutulur; alinamazsa bos liste.""" - simdi = time.time() - if simdi - _GECIT["zaman"] < 600: - return _GECIT["liste"] - _GECIT["zaman"] = simdi - rota = ROUTES.get("aiturk-ide") - if not rota: - _GECIT["liste"] = [] - return [] +def model_oku(govde, kodlama): + """Istekteki model adi; okunamazsa None (o zaman istek Codex'in kendi yoluna gider). Govdeye dokunmaz.""" try: - adres = rota["url"].rsplit("/", 1)[0] + "/models" - istek = urllib.request.Request(adres, headers={"Authorization": f"Bearer {rota['key']}", "User-Agent": UA}) - with urllib.request.urlopen(istek, timeout=6) as yanit: - veri = json.loads(yanit.read()) - _GECIT["liste"] = [("ide/" + m["id"], m["id"]) for m in veri.get("data", []) if isinstance(m, dict) and m.get("id")] - except Exception as e: - log(f"gecit model listesi alinamadi: {type(e).__name__}: {e}") - _GECIT["liste"] = [] - return _GECIT["liste"] + 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() -def model_adi(slug): - return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug class H(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - def log_message(self, *a): pass - def _read(self): + def log_message(self, *a): + pass + + def _oku(self): if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower(): out = bytearray() while True: - raw = self.rfile.readline() - if not raw: - break # baglanti kapandi (EOF): sonsuz donguye girme - line = raw.strip() - if not line: + ham = self.rfile.readline() + if not ham: + break + satir = ham.strip() + if not satir: continue - size = int(line.split(b";")[0], 16) - if size == 0: + boyut = int(satir.split(b";")[0], 16) + if boyut == 0: while self.rfile.readline().strip(): pass break - out += self.rfile.read(size) + 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 _stream(self, resp, status, tag, rewriter=None, izle=None): - self.send_response(status) - for k, v in resp.headers.items(): - if k.lower() in RESP_HOP: continue - self.send_header(k, v) - self.send_header("Transfer-Encoding", "chunked") - self.end_headers() - total = 0 - bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir - def w(b): - if b: - self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush() - try: - while True: - try: - # read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu. - chunk = resp.read1(4096) - except (OSError, http.client.HTTPException) as e: - # Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex - # yarim akisi tam sanmasin ve yeniden denesin. - log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}") - self.close_connection = True - return - if not chunk: break - total += len(chunk) - if len(bas) < 600: - bas += chunk[:600 - len(bas)] - if izle is not None: - izle.feed(chunk) - w(rewriter.feed(chunk) if rewriter else chunk) - if rewriter: w(rewriter.flush()) - self.wfile.write(b"0\r\n\r\n"); self.wfile.flush() - except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError): - log(f"{tag} istemci baglantiyi kapatti ({total} bayt)") - # Yanit bitmeden istemci kapatti (durdurma ya da uygulama kapanmasi): yarim metin sonraki tura tasinir. - if izle is not None and yarim.kaydet(izle): - log(f"{tag} yarim cevap saklandi ({len(izle.metin)} karakter); ayni sohbetin sonraki istegine not olarak eklenecek") - log(f"{tag} bitti status={status} {total} bayt") - if rewriter is not None and total < 600: - log(f"{tag} kisa akis govdesi: {bas[:500]!r}") - - def _tasma_ogren(self, hedef, metin): - sinir = pencere.sinir_oku(metin) - if sinir and pencere.ogren(HOME, hedef, sinir): - log(f"pencere ogrenildi: {hedef} gercek siniri {sinir} token; model listesi yenileniyor") - threading.Thread(target=load_ide_catalog, daemon=True).start() - - def _aracsiz_ogren(self, hedef): - if pencere.ogren_aracsiz(HOME, hedef): - log(f"aracsiz model ogrenildi: {hedef}; model listesinde isaretlenecek") - threading.Thread(target=load_ide_catalog, daemon=True).start() - - def _send_sse_failed(self, kod, mesaj): - """Codex'in akis icinde tanidigi hata bicimi (response.failed + error.code).""" - kimlik = "resp_astrabridge_%d" % int(time.time() * 1000) - olay = {"type": "response.failed", "response": {"id": kimlik, "object": "response", "status": "failed", - "error": {"code": kod, "message": mesaj}, "output": []}} - govde = b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n" - self.send_response(200) - self.send_header("Content-Type", "text/event-stream") - self.send_header("Content-Length", str(len(govde))) - self.end_headers() - self.wfile.write(govde) - - def _send_json(self, status, obj): - b = json.dumps(obj).encode() - self.send_response(status) + 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 _control(self, path): - """Local status/stop for AITURK IDE. Loopback only; carries no secrets.""" - if self.command == "POST" and path.rstrip("/").endswith("/refresh"): - # The IDE asks for this after a sign-in or a settings change, so the model - # list does not wait for the next periodic refresh. - try: - load_ide_catalog() - except Exception as e: - log(f"katalog yenileme hatasi: {e}") - path = "/aiturk/codex-baglama/status" - self.command = "GET" - if self.command == "GET" and path.rstrip("/").endswith("/status"): - durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ") - return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": durum, - "ide_model": int((sayi.split() or ["0"])[0]), - "ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)), - "is_hatti": bool(HAT_KOMUT[0])}) - if self.command == "POST" and path.rstrip("/").endswith("/stop"): - self._send_json(200, {"ok": True}) + 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: (time.sleep(0.3), os._exit(0)), daemon=True).start() + threading.Thread(target=lambda: (KOPRU_SUREC.durdur(), time.sleep(0.2), os._exit(0)), daemon=True).start() return - return self._send_json(404, {"error": {"message": "unknown control path"}}) + 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 _handle(self): - with lock: - seq[0] += 1; n = seq[0] + 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(): - log(f"[{n} ws-upgrade {self.path[:60]}] -> 426, HTTP'ye dusuruluyor") + # 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 - body = self._read() - path = self.path - if path.startswith("/aiturk/codex-baglama/"): - return self._control(path) - if DEBUG: - log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}") - if self.command == "POST" and path.split("?", 1)[0].rstrip("/").endswith("/responses"): - return self._responses(n, path, body) - return self._passthrough(n, path, body) - - def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None, izle=None): - tag = f"[{n} {tagname} {self.command} {path[:80]}]" - hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP} - if rewritten: - hdrs = {k: v for k, v in hdrs.items() if k.lower() != "content-encoding"} - if self.command == "GET" and "/models" in path: - # The upstream validator knows nothing about the added 9Router catalog. - hdrs = {k: v for k, v in hdrs.items() if k.lower() not in ("if-none-match", "if-modified-since")} - req = urllib.request.Request(CHATGPT + path, data=body or None, headers=hdrs, method=self.command) - try: - resp = urllib.request.urlopen(req, timeout=UPSTREAM_IDLE_TIMEOUT); status = resp.status - except urllib.error.HTTPError as e: - resp = e; status = e.code - except Exception as e: - log(f"{tag} HATA {e}") - # Ag yokken de model secici bosalmasin: chatgpt.com'a hic ulasilamazsa son basarili liste verilir. - if self.command == "GET" and "/models" in path and self._son_listeyi_ver(tag, self.headers.get("chatgpt-account-id") or "-"): + 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._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}}) - if self.command == "GET" and "/models" in path: - acct = self.headers.get("chatgpt-account-id") or "-" - if status == 200: - out = self._models(resp, tag) - return out - body = resp.read()[:300] - log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}") - if self._son_listeyi_ver(tag, acct): - return - self.send_response(status) - self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body))) - self.end_headers(); self.wfile.write(body); return - if status >= 400: - errbody = resp.read() - log(f"{tag} chatgpt.com {status}: {errbody[:300]!r}") - self.send_response(status) - for k, v in resp.headers.items(): - if k.lower() in RESP_HOP: continue - self.send_header(k, v) - self.send_header("Content-Length", str(len(errbody))) - self.end_headers(); self.wfile.write(errbody); return - if (self.command == "POST" and "/realtime/calls" in path and status in (200, 201) - and (CFG.get("voice_prep") or {}).get("enabled")): - # Sesli on hazirlayici (bridge.json voice_prep.enabled). Hata atmaz; basliklar yalniz bellekte. - try: - import voice_prep - if voice_prep.baslat(resp.headers.get("Location"), hdrs, os.path.join(HOME, "bridge.json"), LOGDIR): - log(f"{tag} sesli on hazirlayici baslatildi") - except Exception as e: - log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}") - self._stream(resp, status, tag, rewriter, izle) + return self._ilet(n, CHATGPT, yol, govde, "LISTE ozgun", sessiz=True) + return self._ilet(n, CHATGPT, yol, govde, "OZGUN diger", sessiz=True) - def _son_listeyi_ver(self, tag, acct): - """Bu hesabin son basarili model listesini gonderir. True: gonderildi.""" - cached = LAST_MODELS.get(acct) - if not cached: - return False - self.send_response(200) - for k, v in cached["headers"]: - self.send_header(k, v) - self.send_header("Content-Length", str(len(cached["body"]))) - self.end_headers(); self.wfile.write(cached["body"]) - log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})") - return True + do_POST = _isle + do_GET = _isle + do_PUT = _isle + do_DELETE = _isle + do_PATCH = _isle - def _openai_ozet_coz(self, n, oge, ozet_cfg): - """OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz).""" - klasor = os.path.join(LOGDIR, "ozet-onbellek") - metin = ozet_kopru.onbellekten(klasor, oge) - if metin: - return metin - govde = ozet_kopru.coz_istegi(oge, ozet_cfg.get("openai_modeli", "gpt-6-luna")) - hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"} - hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream" - r = urllib.request.Request(CHATGPT + self.path, data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST") - try: - resp = urllib.request.urlopen(r, timeout=float(ozet_cfg.get("coz_zaman_asimi", 300))) - metin = ozet_kopru.sse_metni(resp.read()) - except urllib.error.HTTPError as e: - log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: HTTP {e.code} {e.read()[:200]!r}") - return None - except Exception as e: - log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: {type(e).__name__}") - return None - if metin: - ozet_kopru.onbellege(klasor, oge, metin) - log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)") - return metin or None - def _ozet_openai(self, n, path, req, slug, ozet_cfg, girdi_bayt): - """Kopru modeline (ide/, 9r/) gelen sikistirma isteginin ozetini abonelik modeline yazdirir. - Donus: True = yanit istemciye gitti; False = abonelik istegi baslamadan basarisiz oldu - (istemciye hicbir sey yazilmadi, cagiran hedef modelle devam eder). req cagiranin kopyasidir.""" - model = ozet_cfg.get("ozet_modeli", "gpt-6.1-sol") - tag = f"[{n} CHATGPT {model} <- {slug}]" - try: - req["model"] = model - fix_tool_ids(req) - atilan = drop_foreign_items(req) - ozet_kopru.ozetleri_ac(req, yabanci_hedef=False) - ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem")) - body = json.dumps(req, ensure_ascii=False).encode("utf-8") - except Exception as e: - log(f"{tag} ozet kopru: abonelik ozeti hazirlanamadi ({type(e).__name__}: {e}), hedef model deneniyor") - return False - hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"} - hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream" - r = urllib.request.Request(CHATGPT + path, data=body, headers=hdrs, method="POST") - try: - resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT) - except urllib.error.HTTPError as e: - log(f"{tag} ozet kopru: abonelik ozeti reddedildi (HTTP {e.code} {e.read()[:200]!r}), hedef model deneniyor") - return False - except Exception as e: - log(f"{tag} ozet kopru: abonelik ozeti baslatilamadi ({type(e).__name__}), hedef model deneniyor") - return False - log(f"{tag} ozet kopru: sikistirma ozeti abonelik modeline yazdiriliyor (girdi={len(req.get('input', []))} oge, " - f"{girdi_bayt} bayt, {atilan} yabanci oge atildi)") - akis = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg)) - self._stream(resp, resp.status, tag, akis) - log(f"{tag} ozet kopru: {akis.sonuc_notu()}") - return True - - def _models(self, resp, tag): - raw = resp.read() - try: - raw = decompress(raw, resp.headers.get("Content-Encoding")) - data = json.loads(raw) - models = data.get("models", []) - changed = 0 - # Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz). - by_slug = {m.get("slug"): m for m in models} - ozgun_kimlikler = set(by_slug) - ekler = EXTRA_MODELS # tek kopya: liste hazirlanirken katalog yenilenirse sira ve icerik tutarli kalir - for x in ekler: - if x["slug"] in by_slug: - continue - base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {}) - m = copy.deepcopy(base) - m["slug"] = x["slug"] - m["display_name"] = x.get("display_name", x["slug"]) - m["description"] = x.get("description", "") - m["visibility"] = "list" - m["priority"] = x.get("priority", 0) - m["upgrade"] = None; m["availability_nux"] = None - dis = x["slug"].startswith(IDE_PREFIX) - if not dis: - m["additional_speed_tiers"] = []; m["service_tiers"] = [] - # Dis modelde hiz katmani ilan edilmeye devam eder (yoksa Codex her turda "service tier ... will be - # omitted" uyarisi basar); istekteki service_tier alani gecide gonderilmeden once silinir. - if dis and CFG.get("dis_arac_modu", "direct") == "direct": - # Abonelik kalibi "code_mode_only": tek arac JavaScript calistiran `exec`. Zayif modeller bunu - # cagiramiyor ya da ciktiyi yazdirmiyor (10 Ekim 2026 olcumu). Dis modellere Codex'in klasik - # araclari dogrudan verilir (GPT-5.5 kalibindaki gibi); Responses Lite kapatilir. - m["tool_mode"] = None - m["use_responses_lite"] = False - m["experimental_supported_tools"] = [] - if x.get("context_window"): - cw = int(x["context_window"]) - m["context_window"] = cw; m["max_context_window"] = cw - if x.get("reasoning_levels"): - m["supported_reasoning_levels"] = [l for l in m.get("supported_reasoning_levels", []) if l.get("effort") in x["reasoning_levels"]] - if SLIM_CLONES: - saved = {f_: m[f_] for f_ in SLIM_FIELDS if f_ in m} - for f_ in SLIM_FIELDS: - m.pop(f_, None) - # Codex, bir girdide hem base_instructions hem - # model_messages.instructions_template yoksa listenin TAMAMINI - # reddeder. Kirpma sonrasi ikisi de kalmadiysa geri koy. - mm = m.get("model_messages") - has_tmpl = isinstance(mm, dict) and mm.get("instructions_template") - if not m.get("base_instructions") and not has_tmpl: - m.update(saved) - models.append(m); changed += 1 - if ekler: - ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler) - out = json.dumps(data, ensure_ascii=False).encode("utf-8") - except Exception as e: - log(f"{tag} model listesi degistirilemedi: {e}") - out = raw; changed = 0 - raw_len = len(out) - duz = out # sikistirilmamis govde: yedege bu yazilir - catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"' - enc_out = None - if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed: - import gzip - out = gzip.compress(out, compresslevel=6); enc_out = "gzip" - hdrs_out = [] - for k, v in resp.headers.items(): - kl = k.lower() - if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue - if kl in ("etag", "x-models-etag") and changed: - v = catalog_etag - hdrs_out.append((k, v)) - if changed: - # Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de). - acct_ = self.headers.get("chatgpt-account-id") or "-" - onceki = LAST_MODELS.get(acct_) - LAST_MODELS[acct_] = {"headers": list(hdrs_out), "body": duz, "at": time.strftime("%Y-%m-%d %H:%M:%S")} - # Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz. - if not onceki or onceki.get("body") != duz: - save_last_models(acct_, list(hdrs_out), duz) - if enc_out: - hdrs_out.append(("Content-Encoding", enc_out)) - self.send_response(200) - for k, v in hdrs_out: - self.send_header(k, v) - self.send_header("Content-Length", str(len(out))) - self.end_headers() - self.wfile.write(out) - log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi") - - def _yerel_yanit(self, slug, metin): - govde = hazir_yaniti(slug, metin) - self.send_response(200) - self.send_header("Content-Type", "text/event-stream") - self.send_header("Content-Length", str(len(govde))) - self.end_headers() - self.wfile.write(govde) - - def _model_komutu(self, n, req, sohbet, gelen): - """Son kullanici mesaji "modeli X yap" / "hangi model" ise isler. Yerel cevap metni doner; - None: istek (gerekirse degistirilmis olarak) modele gider.""" - metin, sira, sesli = model_gecis.son_kullanici_metni(req) - komut = model_gecis.komut_coz(metin) if metin else None - if not komut: - return None - etkin = yonlendirilen(sohbet, gelen) or gelen - ortada = model_gecis.tur_ortasi(req, sira) - if komut["tur"] == "sor": - if ortada: - return None - log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}") - return f"Sohbetin modeli şu an {model_adi(etkin)}." - if komut["tur"] == "nasil": - if ortada: - return None - log(f"[{n} {gelen}] adsiz model komutu: nasil soylenecegi anlatildi") - return f"Sohbetin modeli şu an {model_adi(etkin)}. " + model_gecis.NASIL - # Sesle gecis yalniz bizim ekledigimiz modeller arasindadir; Codex'in kendi modelleri aday degildir - # (kullanici karari, 11 Ekim 2026). Iki taraf arasinda gecis: ekrandaki seciciden model secilir, "." yazilir. - bilinen = bilinen_modeller() - sonuc = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if ek_model_mu(s)]) - if not sonuc: - sonuc = model_gecis.eslestir(komut["ad"], gecit_modelleri()) # Codex listesinde olmayan gecit modeli - if not sonuc: - ozgun = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if not ek_model_mu(s)]) - if ozgun and (komut.get("kesin", True) or "slug" in ozgun): - kim = f"{ozgun['ad']} Codex'in kendi modeli" if "ad" in ozgun else "Bu ad Codex'in kendi modellerine uyuyor" - ileti = (f"{kim}; Codex'in kendi modellerine sesle geçilmiyor, model değişmedi. Geçmek için ekrandaki " - f"model seçiciden o modeli seç ve tek başına \".\" yaz. Sohbetin modeli hâlâ {model_adi(etkin)}.") - log(f"[{n} {gelen}] model komutu: Codex'in kendi modeli istendi {komut['ad']}, gecilmedi") - if not ortada: - return ileti - if not komut.get("kesin", True): - return None - model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı Codex'in kendi modeline geçmek istedi; bu sesle " - "yapılamıyor, model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + ileti + "]") - self.komut_govdeyi_degistirdi = True - return None - if not komut.get("kesin", True) and not (sonuc and "slug" in sonuc): - return None # "modeli" denmemis ve ad tek modele oturmuyor: komut degil, modele gider - if not sonuc: - if ortada: - return None - # Acik komut ("modeli ... yap") ama ad bulunamadi: model "kopru yapacak" diye yanlis soz vermesin. - log(f"[{n} {gelen}] model komutu: ad bulunamadi {komut['ad']}") - return (f"\"{' '.join(komut['ad'])}\" adına uyan bir model bulamadım, model değişmedi; sohbetin modeli hâlâ " - f"{model_adi(etkin)}. Adını biraz farklı söyler misin?") - if "adaylar" in sonuc: - soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: " - + ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?") - log(f"[{n} {gelen}] model komutu belirsiz: {komut['ad']} -> {sonuc['adaylar']}") - if not ortada: - return soru - model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; " - "model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]") - self.komut_govdeyi_degistirdi = True - return None - hedef = sonuc["slug"] - if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES: - return "AITURK bağlantısı şu an kapalı; bu modele geçemedim, model değişmedi." - YONLENDIRME.koy(sohbet, gelen, hedef) - log(f"[{n} {gelen}] model komutu: {model_adi(etkin)} -> {sonuc['ad']}" - f" ({'tur ortasi, is devrediliyor' if ortada else 'bosta'}{', sesli' if sesli else ''})") - if hedef == etkin: - return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}." - if ortada: - model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin))) - self.komut_govdeyi_degistirdi = True - return None - # Listede ayni modeli yeniden secmek yonlendirmeyi kaldirmaz (Codex ayni adi gonderir); geri donus yolu soylenir. - donus = f" Listede seçili modele dönmek için \"modeli {model_adi(gelen)} yap\" de." if hedef != gelen else "" - return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. " - f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}") - - def _ozgun(self, n, path, body, req, slug): - """Codex'in kendi (abonelik) modeli: kopru HICBIR SEY yapmaz, istek chatgpt.com'a bayt bayt oldugu gibi gider. - Kullanici karari (11 Ekim 2026): "Codex kendi modelini kullanirken kopruyle bir bagi olmamali ... Codex'in - kendi modeline dokunulmamali" ve "Codex'i secip Codex'e yaziyorsan bizim kopru yardim amacli bile yaninda - olamaz." Bu yuzden model komutu, nokta karsilama, bilgi notu, is hatti talimati, yarim cevap notu, - sikistirmaya karisma ve koprunun kendi izlerini temizleme de YOKTUR; sikistirmayi Codex kendisi yapar - (olcum 11 Ekim 2026: kopru ozgun modelde ozeti kendi uretmeye calisip Codex'i "Error running remote compact - task" ile takti). - Bilinen sonuc: ayni sohbette daha once ek model kullanildiysa ya da kopru ozeti kaldiysa chatgpt.com bu - istegi reddedebilir; o sohbet ek modellerle surer, Codex'in kendi modeli icin yeni sohbet acilir.""" - return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=False) - - def _responses(self, n, path, body): - try: - req = json.loads(decompress(body, self.headers.get("Content-Encoding"))) - except Exception as e: - log(f"[{n} responses] govde cozulemedi: {e}") - return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}}) - slug = req.get("model", "") - gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model) - if not ek_model_mu(gelen): - return self._ozgun(n, path, body, req, slug) - govde_degisti = False # istek govdesi kopruce degistirildi: abonelik yolunda yeniden yazilir - once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True) - sohbet = str(req.get("prompt_cache_key") or "") - if CFG.get("sesle_model", True) and sohbet: - self.komut_govdeyi_degistirdi = False - try: - yerel = self._model_komutu(n, req, sohbet, gelen) - except Exception as e: - yerel = None - log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}") - if yerel is not None: - return self._yerel_yanit(gelen, yerel) - if self.komut_govdeyi_degistirdi: - govde_degisti = True # komut mesaji nota cevrildi: abonelik yolunda da degismis govde gider - try: - if model_gecis.gecmisi_temizle(req, bilinen_modeller(), yonlendirilen(sohbet, gelen) or gelen): - govde_degisti = True - except Exception as e: - log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}") - hedef = yonlendirilen(sohbet, gelen) - if hedef and hedef != gelen: - req["model"] = slug = hedef - log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor") - mapping = MODEL_MAP.get(slug) - if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])): - # Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida - # keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar. - del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir - govde_degisti = True - log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi") - if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)): - govde_degisti = True - if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req): - # Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da - # istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur. - log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)") - return self._yerel_yanit(gelen, f"{model_adi(slug)} seçildi. Sohbetin modeli artık {model_adi(slug)}.") - # Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py). - kaynak = self.headers.get("originator") or "" - if kaynak and kaynak not in GORULEN_ISTEMCI: - GORULEN_ISTEMCI.add(kaynak) - log(f"istemci goruldu: {kaynak}") - try: - # Model, sohbetin modelinin soylenerek degistirilebildigini bilsin; yoksa "degistiremem" diyor. - if CFG.get("sesle_model", True) and CFG.get("model_bilgi", True) and sohbet and model_gecis.bilgi_ekle(req): - govde_degisti = True - if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle( - req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)): - govde_degisti = True - except Exception as e: - log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}") - # Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir. - yarim_eklenen = 0 - if CFG.get("yarim_cevap", True): - try: - yarim_eklenen = yarim.uygula(req, sohbet) - except Exception as e: - log(f"[{n} {slug}] yarim cevap notu atlandi: {type(e).__name__}: {e}") - if yarim_eklenen: - log(f"[{n} {slug}] yarim cevap notu eklendi ({yarim_eklenen} karakter)") - izle = yarim.Izleyici(sohbet, slug) if (sohbet and CFG.get("yarim_cevap", True)) else None - try: - if not ozet_kopru.tetik_mi(req): - OZET_FRENI.normal(sohbet or slug) # ozet olmayan istek: art arda ozet sayaci sifirlanir - except Exception: - pass - ozet_cfg = CFG.get("ozet_kopru") or {} - ozet_acik = ozet_cfg.get("enabled", False) - catalog = CFG.get("nine_router_catalog") or {} - prefix = catalog.get("prefix", "9r/") - if mapping is None and prefix and slug.startswith(prefix): - if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES: - # Newly added API models can be selected before the background refresh. - # Provider-qualified IDs are passed intact to 9Router, never to ChatGPT. - mapping = (catalog.get("route", "9router"), slug[len(prefix):], None) - else: - # Rotasi kalmamis eski ad chatgpt.com'a baska bir adla gonderilmez ("model desteklenmiyor" donuyordu). - log(f"[{n} {slug}] bu on ekin rotasi yok; istek gonderilmedi") - return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}}) - ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler - if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES: - mapping = (ide_route, slug[len(ide_prefix):], None) - if mapping is None and slug.startswith(ide_prefix): - # Never send an IDE model to chatgpt.com under another name. - log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok") - # 503 donmek Codex'i sessizce yeniden denetiyor ve tur cevapsiz bitiyordu; sesli asistan da "bakiyorum" - # deyip duruyordu (olcum 11 Ekim 2026, sunucu gecisi sirasinda). Kullanici durumu duysun. - if CFG.get("kesinti_mesaji", True): - return self._yerel_yanit(gelen, "AITURK modellerine şu an ulaşılamıyor (AITURK sunucusu cevap vermiyor ya da " - "bağlantı kapalı); bu model şimdi cevap veremez. Beklemek istemezsen ekrandaki model " - "seçiciden Codex'in kendi modellerinden birini seç ve tek başına \".\" yaz.") - return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}}) - if mapping is None: - # Codex'in kendi modelleri en basta _ozgun ile ayrildi; buraya yalniz rotasi cozulemeyen bir ek ad duser. - # Ek model chatgpt.com'a baska bir adla gonderilmez. - log(f"[{n} {slug}] ek modelin rotasi cozulemedi; istek gonderilmedi") - return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}}) - route_name, target, flat = mapping - try: - ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req) - except Exception as e: - ozet_tetik = False; ozet_acik = False - log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}") - girdi_bayt = 0 - if ozet_tetik: - OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3)) - if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()): - log(f"[{n} {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)") - return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir, - "type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}}) - girdi_bayt = len(json.dumps(req.get("input") or [], ensure_ascii=False).encode("utf-8")) - if ozet_tetik and ozet_cfg.get("model"): - # Ozeti baglami alabilen baska bir modele yazdirmak icin (ornegin kucuk hedef kendi basina ozetleyemiyorsa). - alt = MODEL_MAP.get(ozet_cfg["model"]) - if alt: - route_name, target, flat = alt - elif ozet_tetik and ozet_cfg.get("ozetleyen", "openai") == "openai": - # Ozeti secili kopru modeli degil, abonelikteki guclu model yazar (10 Ekim 2026: 1 MB'lik - # sohbeti kucuk bir model 2201 karaktere indirdi ve model degisince hafiza gitti). - # Abonelik istegi baslamadan reddedilirse asagidaki eski yol (hedef model) calisir. - if self._ozet_openai(n, path, copy.deepcopy(req), slug, ozet_cfg, girdi_bayt): - return - # 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir. - via_router = route_name in (catalog.get("route", "9router"), ide_route) - route = ROUTES.get(route_name) - if route is None: - # Katalog yenilemesi rotayi bu istek hazirlanirken kaldirdi (baglama kapandi ya da IDE oturumu gitti). - log(f"[{n} {slug}] rota artik yok: {route_name}") - return self._send_json(503, {"error": {"message": "Bu modelin baglantisi su an kapali; birazdan yeniden deneyin ya da baska bir model secin."}}) - tag = f"[{n} {route_name.upper()} {slug}->{target}]" - req["model"] = target - req.pop("client_metadata", None) - if route_name == ide_route: - req.pop("service_tier", None) # abonelik hiz katmani; gecit tanimaz - for k in route.get("strip_fields", []): - req.pop(k, None) - if ozet_acik: - ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True, coz=lambda oge: self._openai_ozet_coz(n, oge, ozet_cfg)) - if ob["acilan"] or ob["cozulen"] or ob["okunamayan"]: - log(f"{tag} ozet kopru: kopru ozeti={ob['acilan']}, OpenAI ozeti duz metne cevrildi={ob['cozulen']}, okunamayan={ob['okunamayan']}") - if ozet_tetik: - ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem")) - log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)") - if via_router and CFG.get('resim_sadelestir', True): - # Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026). - # Duzlestirmeden ONCE: kimliksiz arac ciktisi orada duz metne cevrilir ve icindeki resim taninmaz olur. - cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel"))) - if cikan: - log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)") - rewriter = None - if flat if flat is not None else route.get("flatten_tools"): - custom = flatten_request(req) - duzelen = arac_semalarini_duzelt(req) - if duzelen: - log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)") - rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren) - fix_tool_ids(req) - if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'): - merged = coalesce_context(req) - if merged: - log(f"{tag} Gemini instruction context: {merged} message(s) preserved") - elif via_router and CFG.get('pano_kanca_user', True): - # 9Router Claude cevirisi developer mesajlarini dusuruyor; bu turun PANO OLAYLARI blogu (sesli_pano.py) - # sona user mesaji olarak eklenir (kanca_context.py, 24 Eylul 2026). bridge.json "pano_kanca_user": false kapatir. - eklenen, ozet = pano_blogunu_ulastir(req) - if eklenen: - log(f"{tag} PANO OLAYLARI blogu user mesaji olarak eklendi ({eklenen} karakter, {ozet})") - if via_router: - # Claude "assistant message prefill" desteklemiyor (25 Eylul 2026, sesli tur kapaninca 400). - eklendi, sonlar = claude_sonu_duzelt(req) - if eklendi: - log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi") - hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere") - # Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz. - # Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir). - pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere) - if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik: - kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude")))) - if rewriter is not None: - rewriter.dolu = tahmin >= hedef_pencere * 0.7 - if kirpilan: - log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})") - if not sigdi: - log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})") - return self._send_sse_failed("context_length_exceeded", - f"Bu mesaj seçili modelin bağlam penceresine sığmıyor (yaklaşık {tahmin} token, pencere {hedef_pencere}). " - "Daha büyük pencereli bir model seçin ya da mesajı kısaltın.") - data = json.dumps(req, ensure_ascii=False).encode("utf-8") - log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt") - if DEBUG: - open(os.path.join(LOGDIR, f"{n:04d}_req.json"), "wb").write(data) - hdrs = {"Authorization": f"Bearer {route['key']}", "Content-Type": "application/json", "Accept": "text/event-stream", "User-Agent": UA} - last = None - # A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may - # have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026); - # only failures before the work starts (no connection, 500/502/503/529) are retried. - combo = "/" not in target - for attempt in range(1, RETRIES + 1): - r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST") - try: - resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT) - except urllib.error.HTTPError as e: - errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir - last = (e.code, errbody[:400]) - etxt = errbody.decode("utf-8", "replace") - # 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak - # donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu. - permanent = (any(p.lower() in etxt.lower() for p in NO_RETRY_PATTERNS) - or bool(re.search(r"\[4(?!29|08)\d\d\]", etxt))) - if permanent: - log(f"{tag} kalici hata, tekrar denenmiyor: HTTP {e.code} {errbody[:160]!r}") - # 524 = Cloudflare gave up waiting while the origin kept working; a retry - # re-runs (and re-bills) the whole Fusion (22 Sep 2026: 3 double runs). - elif e.code in ((500, 502, 503, 529) if combo else (500, 502, 503, 504, 529)) and attempt < RETRIES: - log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar") - time.sleep(min(2 * attempt, 8)); continue - log(f"{tag} HTTP {e.code} {errbody[:200]!r}") - if pencere.tasma_mi(etxt): - # Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin - # bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur). - self._tasma_ogren(target, etxt) - return self._send_sse_failed("context_length_exceeded", etxt[:300]) - self.send_response(e.code) - self.send_header("Content-Type", "application/json") - bekle = e.headers.get("Retry-After") if e.headers is not None else None - if bekle: - self.send_header("Retry-After", str(bekle)) # 429'da ne kadar beklenecegi istemciye ulassin - self.send_header("Content-Length", str(len(errbody))) - self.end_headers(); self.wfile.write(errbody); return - except Exception as e: - last = (0, str(e)) - if combo and is_timeout(e): - log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}") - break - if combo and kopma_mi(e): - log(f"{tag} combo istegi gonderildikten sonra baglanti koptu, tekrar gonderilmiyor (calisiyor olabilir): {e}") - break - log(f"{tag} deneme {attempt}: {e}") - time.sleep(min(2 * attempt, 8)); continue - # Headers go to the client from here on: a cut stream is never resent. - if ozet_tetik: - akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg)) - sonuc = self._stream(resp, resp.status, tag, akis) - log(f"{tag} ozet kopru: {akis.sonuc_notu()}") - return sonuc - return self._stream(resp, resp.status, tag, rewriter, izle) - self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}}) - - do_POST = _handle - do_GET = _handle - do_PUT = _handle - do_DELETE = _handle - do_PATCH = _handle - -class TS(http.server.ThreadingHTTPServer): +class Sunucu(http.server.ThreadingHTTPServer): daemon_threads = True - # Windows'ta SO_REUSEADDR baska bir surecin ayni portu "calmasina" izin verir. - # Kapali: port doluysa sessizce yan yana dinlemek yerine acikca hata verir. - allow_reuse_address = False + allow_reuse_address = False # kapi doluysa sessizce yan yana dinlemek yerine acikca hata + if __name__ == "__main__": - load_last_models() - log(f"AstraBridge :{PORT} routes={list(ROUTES)} model_map={len(MODEL_MAP)} diger->{CHATGPT}") try: - srv = TS(("127.0.0.1", PORT), H) + sunucu = Sunucu(("127.0.0.1", PORT), H) except OSError as e: - log(f"HATA: 127.0.0.1:{PORT} baglanamadi ({e}). Port baska bir surecte olabilir; " - f"bridge.json icindeki 'port' degerini ve config.toml'daki openai_base_url'i degistirin.") + log(f"HATA: 127.0.0.1:{PORT} acilamadi ({e}); baska bir surec bu kapiyi kullaniyor olabilir.") sys.exit(1) - srv.serve_forever() + 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() diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py new file mode 100644 index 0000000..2fb6a20 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -0,0 +1,1566 @@ +"""AITURK IDE Codex bridge. + +A small local HTTP proxy between OpenAI Codex (desktop or CLI) and its backend. +AITURK IDE's "Codex Baglama" setting points Codex's `openai_base_url` at it. + +- POST .../responses -> by model slug: + * subscription models (no mapping) -> chatgpt.com, unchanged + * "ide/" models -> the AITURK gateway the IDE itself uses +- GET .../models -> the IDE's models and combos are added to Codex's own list +- everything else -> chatgpt.com, unchanged (voice, remote, auth) +- Conversation summaries (compaction) are produced as plain text so that every + provider can read them when the user switches models (ozet_kopru.py). + +Data directory (bridge.json, log/): --home or AITURK_CODEX_BRIDGE_HOME, +otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME. +Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop. +""" +import http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib +from gemini_context import coalesce_context +try: + from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context +except ImportError: + def pano_blogunu_ulastir(req): + return 0, "" +from resim_context import resimleri_sadelestir +import ide_catalog +import ozet_kopru +import pencere +import yarim +import model_gecis +import is_hatti + +SURUM = "8" +HERE = os.path.dirname(os.path.abspath(__file__)) +try: + with open(os.path.abspath(__file__), "rb") as _f: + KOD = hashlib.sha256(_f.read()).hexdigest()[:8] # ayni surum numarasi altindaki farkli kodu ayirt eder +except OSError: + KOD = "" + +# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older +# interpreters the matching `zstandard` build ships next to this file. +_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_decompress(body): + try: + from compression import zstd as _z + return _z.decompress(body) + except ImportError: + import zstandard + return zstandard.ZstdDecompressor().decompressobj().decompress(body) + + +def _zstd_ready(): + try: + try: + from compression import zstd as _z + sample = _z.compress(b"aiturk") + except ImportError: + import zstandard + sample = zstandard.ZstdCompressor().compress(b"aiturk") + return _zstd_decompress(sample) == b"aiturk" + except Exception: + return False + + +# Without zstd every Codex request would fail; the status endpoint then reports +# ok=false so that the IDE never points Codex at this bridge. +ZSTD_OK = _zstd_ready() + + +def _arg(name, env): + """--name from the command line, else the environment variable.""" + 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 +if _arg("--hermes-home", "HERMES_HOME"): + os.environ["HERMES_HOME"] = _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 = {} # no settings file: subscription models pass through, IDE catalog on +# Ayiricinin (bridge.py) arkasinda calisirken kapi --port ile verilir; tek basina calisirken bridge.json'daki kapidir. +PORT = int(_arg("--port", "AITURK_CODEX_KOPRU_PORT") or CFG.get("port", 8790)) +CHATGPT = CFG.get("chatgpt_backend", "https://chatgpt.com").rstrip("/") +ROUTES = dict(CFG.get("routes") or {}) # {"cheapvibecode": {"url","key","flatten_tools"}, ...} +RETRIES = int(CFG.get("retries", 6)) +# Socket idle (per read) timeout toward 9Router/chatgpt.com. 9Router and OmniRoute keep +# long answers alive with a byte every 15 s, so this only fires on a dead peer; 60+ min +# per user decision (28 Sep 2026) so a heavy Fusion is never cut here. +UPSTREAM_IDLE_TIMEOUT = float(CFG.get("upstream_idle_timeout", 3600)) + +def is_timeout(error): + """True for a socket/read timeout, also when urllib wraps it in URLError.""" + reason = getattr(error, "reason", None) + return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError) +def kopma_mi(error): + """Baglanti kurulduktan sonra uzak uc kapatti ya da sifirladi (yanit basliklari gelmeden): istek ulasmis ve + is baslamis olabilir. Baglanti hic kurulamadiysa (reddedildi, DNS) False.""" + reason = getattr(error, "reason", None) + turler = (ConnectionResetError, ConnectionAbortedError, http.client.IncompleteRead) # RemoteDisconnected dahil + return isinstance(error, turler) or isinstance(reason, turler) +DEBUG = bool(CFG.get("debug", False)) +UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)") +# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir. +NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"]) +LOGDIR = os.path.join(HOME, "log") +os.makedirs(LOGDIR, exist_ok=True) +lock = threading.Lock() +seq = [0] + +def log(msg): + line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg + print(line, flush=True) + with lock: + with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f: + f.write(line + "\n") + +# ---- model yonlendirme tablosu: slug -> (route, hedef model) ---- +MODEL_MAP = {} # slug -> (route, hedef model, flatten_tools or None) +for slug, v in CFG.get("model_map", {}).items(): + if isinstance(v, str): + v = {"route": "cheapvibecode", "model": v} + MODEL_MAP[slug] = (v["route"], v["model"], v.get("flatten_tools")) + +EXTRA_MODELS = list(CFG.get("extra_models", [])) +for m in EXTRA_MODELS: + MODEL_MAP.setdefault(m["slug"], (m.get("route", "cheapvibecode"), m.get("target", m["slug"]), m.get("flatten_tools"))) + +def load_9router_catalog(): + global MODEL_MAP, EXTRA_MODELS + cat = CFG.get("nine_router_catalog") or {} + if not cat.get("enabled"): + return + route = cat.get("route", "9router") + r = ROUTES.get(route) + if not r: + return + prefix = cat.get("prefix", "9r/") + try: + req = urllib.request.Request(r["url"].rsplit("/responses", 1)[0] + "/models", + headers={"Authorization": f"Bearer {r['key']}", "User-Agent": UA}) + data = json.load(urllib.request.urlopen(req, timeout=30)) + except Exception as e: + log(f"9Router katalogu alinamadi: {e}") + return + model_map = {k: v for k, v in MODEL_MAP.items() if not k.startswith(prefix)} + extra_models = [m for m in EXTRA_MODELS if not m["slug"].startswith(prefix)] + n = 0 + # Secicide yalniz "saglayici/model" gorunur (kullanici istegi, 9 Ekim 2026). + adlar = ide_catalog.adlandir([(m.get("id", ""), m.get("id", "")) for m in data.get("data", []) if m.get("id")]) + for m in data.get("data", []): + mid = m.get("id", "") + slug = prefix + mid + if not mid or slug in model_map: + continue + extra_models.append({"slug": slug, "display_name": adlar.get(mid, mid), + "description": f"9Router: {mid}", "route": route, "target": mid, + "clone_from": cat.get("clone_from", "gpt-6-astra"), + "priority": 100 + n, "reasoning_levels": cat.get("reasoning_levels"), + "context_window": m.get("context_window") or m.get("context_length")}) + model_map[slug] = (route, mid, None) + n += 1 + MODEL_MAP = model_map + EXTRA_MODELS = extra_models + log(f"9Router katalogu: {n} model eklendi") + +load_9router_catalog() + +# ---- AITURK IDE "Codex Baglama": IDE'nin model/combolari (ide_catalog.py, 9 Ekim 2026) ---- +IDE_PREFIX = "ide/" +IDE_ROUTE = "aiturk-ide" +IDE_DURUM = [None] +IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz) + +def load_ide_catalog(ag=True): + """IDE'nin gecit katalogunu "ide/" olarak ekler. Ayar kapaliysa ya da IDE anahtari + yoksa girdiler kaldirilir; gecit gecici olarak cevap vermezse son liste korunur.""" + global MODEL_MAP, EXTRA_MODELS + try: + durum, base, anahtar, girdiler = ide_catalog.yukle(UA, home=HOME, ag=ag) + except Exception as e: + log(f"AITURK IDE katalogu alinamadi, son liste korunuyor: {e}") + return + model_map = {k: v for k, v in MODEL_MAP.items() if not k.startswith(IDE_PREFIX)} + extra_models = [m for m in EXTRA_MODELS if not m["slug"].startswith(IDE_PREFIX)] + n = 0 + if durum == "tamam": + # Anahtar yalniz bellekte tutulur; bridge.json'a yazilmaz. + ROUTES[IDE_ROUTE] = {"url": base + "/responses", "key": anahtar, "flatten_tools": True} + clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra") + levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels") + # Gorunen adlari ayirt et; farkli kimlikteki modeller adlari benziyor diye silinmez. + var_olan = {m.get("display_name", "").lower() for m in extra_models} + adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan, + turler={g["id"]: g["kind"] for g in girdiler}) + for g in girdiler: + slug = IDE_PREFIX + g["id"] + if slug in model_map: + continue + extra_models.append({"slug": slug, "display_name": adlar[g["id"]], + "description": g.get("aciklama") or f"AITURK IDE: {g['ad']}", + "route": IDE_ROUTE, "target": g["id"], + "clone_from": clone, "priority": 300 + n, "reasoning_levels": levels, + "kind": g["kind"], "context_window": g.get("pencere")}) + IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"), + "kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))} + model_map[slug] = (IDE_ROUTE, g["id"], None) + n += 1 + else: + ROUTES.pop(IDE_ROUTE, None) + MODEL_MAP = model_map + EXTRA_MODELS = extra_models + ozet = f"{durum}, {n} model" + if IDE_DURUM[0] != ozet: # 5 dakikada bir ayni satiri yazmamak icin + IDE_DURUM[0] = ozet + log(f"AITURK IDE Codex Baglama: {ozet}") + +# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler); +# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir. +load_ide_catalog(ag=False) + +def catalog_refresh_loop(): + """Kataloglari arka planda yeniler; liste istegini yavaslatmaz.""" + minutes = max(1, int(CFG.get("catalog_refresh_min", 5))) + time.sleep(3) + try: + load_ide_catalog() # acilistan hemen sonra: guncel pencere verisiyle + except Exception as e: + log(f"katalog yenileme hatasi: {e}") + while True: + time.sleep(minutes * 60) + for yukle in (load_9router_catalog, load_ide_catalog): + try: + yukle() + except Exception as e: + log(f"katalog yenileme hatasi: {e}") + +threading.Thread(target=catalog_refresh_loop, daemon=True).start() + +# ---- AITURK is hatti: isi gonderebilen AITURK komutu (is_hatti.py). Kurulum sonradan gelebilir: yoksa 2 dk'da bir bakilir. +HAT_KOMUT = [None] + +def hat_komut_loop(): + while True: + try: + if (CFG.get("aiturk_hat") or {}).get("enabled") is not False: + bulunan = is_hatti.komut_bul() + if bulunan != HAT_KOMUT[0]: + HAT_KOMUT[0] = bulunan + log(f"AITURK is hatti: {'acik, komut ' + bulunan if bulunan else 'kapali (aiturk is komutu yok)'}") + except Exception as e: + log(f"AITURK is hatti komutu aranamadi: {type(e).__name__}: {e}") + time.sleep(600 if HAT_KOMUT[0] else 120) + +threading.Thread(target=hat_komut_loop, daemon=True).start() + +LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir) +CACHE_DIR = os.path.join(LOGDIR, "son-liste") + +def _cache_path(acct): + guvenli = re.sub(r"[^A-Za-z0-9._-]", "_", acct or "-")[:64] + return os.path.join(CACHE_DIR, f"models-{guvenli}.json") + +def save_last_models(acct, hdrs, body): + """Son basarili listeyi diske yazar. Kopru yeniden baslayinca bellek + sifirlandigi icin, restarttan sonraki ilk 401 listeyi silip goturuyordu.""" + try: + os.makedirs(CACHE_DIR, exist_ok=True) + yol = _cache_path(acct) + veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"), + "body": body.decode("utf-8")} + gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # ayni hesabin iki es zamanli kaydi carpismasin + with open(gecici, "w", encoding="utf-8") as f: + json.dump(veri, f, ensure_ascii=False) + os.replace(gecici, yol) # yarim dosya birakmamak icin + except Exception as e: + log(f"son liste diske yazilamadi: {e}") + +def load_last_models(): + """Acilista diskteki son listeleri bellege alir.""" + try: + if not os.path.isdir(CACHE_DIR): + return + for ad in os.listdir(CACHE_DIR): + if not (ad.startswith("models-") and ad.endswith(".json")): + continue + try: + with open(os.path.join(CACHE_DIR, ad), encoding="utf-8") as f: + v = json.load(f) + acct = v.get("acct") or ad[len("models-"):-len(".json")] + LAST_MODELS[acct] = {"headers": [tuple(h) for h in v["headers"]], + "body": v["body"].encode("utf-8"), "at": v.get("at", "?")} + log(f"diskten son liste alindi ({acct}, {len(v['body'])} bayt, {v.get('at')})") + except Exception as e: + log(f"son liste okunamadi {ad}: {e}") + except Exception as e: + log(f"son liste klasoru okunamadi: {e}") +SLIM_CLONES = bool(CFG.get("slim_clones", False)) +SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"]) + +# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ---- +def sema_duzelt(sema): + """Arac parametre semasinin EN UST duzeyindeki oneOf/anyOf/allOf'u tek nesne semasina indirir. + Claude bunu reddediyor ("input_schema does not support oneOf, allOf, or anyOf at the top level"; olcum + 11 Ekim 2026: masaustu Codex'in klasik arac listesindeki bir arac yuzunden her Opus istegi 400 aldi). + Secenek nesnelerinin alanlari birlestirilir; zorunlu alanlar oneOf/anyOf'ta kesisim, allOf'ta birlesimdir. + Donus: (sema, degisti mi).""" + if not isinstance(sema, dict) or not any(k in sema for k in ("oneOf", "anyOf", "allOf")): + return sema, False + yeni = {k: v for k, v in sema.items() if k not in ("oneOf", "anyOf", "allOf")} + alanlar = dict(yeni.get("properties") or {}) + zorunlu = set(yeni.get("required") or []) + for anahtar in ("oneOf", "anyOf", "allOf"): + secenekler = [x for x in (sema.get(anahtar) or []) if isinstance(x, dict)] + kesisim = None + for secenek in secenekler: + secenek, _ = sema_duzelt(secenek) + for ad, tanim in (secenek.get("properties") or {}).items(): + alanlar.setdefault(ad, tanim) + gerekli = set(secenek.get("required") or []) + if anahtar == "allOf": + zorunlu |= gerekli + else: + kesisim = gerekli if kesisim is None else (kesisim & gerekli) + if anahtar != "allOf" and kesisim: + zorunlu |= kesisim + yeni["type"] = "object" + yeni["properties"] = alanlar + if zorunlu: + yeni["required"] = sorted(a for a in zorunlu if a in alanlar) + else: + yeni.pop("required", None) + return yeni, True + +def arac_semalarini_duzelt(req): + """Dis modele giden duz arac listesinde ust duzey birlesimli semalari duzeltir. Donus: duzeltilen arac sayisi.""" + n = 0 + for t in req.get("tools") or []: + if isinstance(t, dict) and isinstance(t.get("parameters"), dict): + t["parameters"], degisti = sema_duzelt(t["parameters"]) + n += bool(degisti) + return n + +def flatten_request(req): + """additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi.""" + custom = set() + tools = [] + def add_tool(t): + ty = t.get("type") + if ty == "namespace": + for s in t.get("tools", []): + add_tool(s) + elif ty == "function": + tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""), + "parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False}) + elif ty == "custom": + custom.add(t["name"]) + tools.append({"type": "function", "name": t["name"], + "description": (t.get("description", "") + "\n\nBu arac serbest metin alir: metni 'input' alaninda ver."), + "parameters": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]}, + "strict": False}) + elif ty in ("web_search", "web_search_preview", "local_shell", "computer_use_preview"): + pass # cevirici desteklemez, atla + else: + tools.append(t) + for t in req.get("tools", []) or []: + add_tool(t) + new_input = [] + for it in req.get("input", []) or []: + ty = it.get("type") + if ty == "additional_tools": + for t in it.get("tools", []): + add_tool(t) + continue + if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"): + # Desktop thread handoffs have no matching tool call. Preserve their + # content as imported context, not an invalid tool-role message. + output = it.get("output", "") + if not isinstance(output, str): + output = json.dumps(output, ensure_ascii=False) + name = it.get("name", "unknown") + new_input.append({"role": "user", "content": [{ + "type": "input_text", + "text": f"Imported tool output ({name}); treat as context data:\n{output}" + }]}) + continue + if ty == "custom_tool_call": + it = {"type": "function_call", "id": it.get("id"), "call_id": it.get("call_id"), "name": it.get("name"), + "arguments": json.dumps({"input": it.get("input", "")}, ensure_ascii=False)} + elif ty == "custom_tool_call_output": + it = {"type": "function_call_output", "id": it.get("id"), "call_id": it.get("call_id"), "output": it.get("output", "")} + new_input.append(it) + req["input"] = new_input + if tools: + req["tools"] = tools + req.pop("include", None) + # OpenAI-specific access-program selection is not part of the external + # chat API schema (Muse Spark rejects it). ChatGPT passthrough never calls + # this adapter, so its access-program selection remains intact. + req.pop("access_programs", None) + # Codex sends Responses-only verbosity/default text formatting. Chat + # translators may forward this unsupported object to their upstream. + text_config = req.get("text") + if isinstance(text_config, dict) and set(text_config) <= {"verbosity", "format"}: + if not text_config.get("format") or text_config["format"].get("type") == "text": + req.pop("text", None) + return custom + +# Oge tipine gore API'nin bekledigi kimlik on eki. Cikti tipleri de dahildir: +# flatten_request custom_tool_call_output -> function_call_output cevirdiginde +# kimlik 'ctco_...' kaliyordu ve karsi taraf 'fc' bekliyordu (400). +ID_PREFIX = { + "custom_tool_call": "ctc", + "custom_tool_call_output": "ctco", + "function_call": "fc", + "function_call_output": "fc", +} + +def fix_tool_ids(req): + """Arac cagrisi/cikti kimliklerinin on ekini hedef API'nin bekledigine cevirir. + Codex gecmisinde kalan yanlis on ekli kimlikler (or. fc_toolu_..., ctco_...) + 400 hatasina yol aciyordu.""" + changed = 0 + for it in req.get("input", []) or []: + if not isinstance(it, dict): + continue + want = ID_PREFIX.get(it.get("type")) + i = it.get("id") + if not want or not isinstance(i, str) or not i: + continue + # Alt cizgi ile karsilastir: 'ctco_x'.startswith('ctc') dogru dondugu icin + # ctco kimlikleri yanlislikla "zaten dogru" sayiliyordu. + if i.startswith(want + "_"): + continue + tail = i.split("_", 1)[1] if "_" in i else i + it["id"] = want + "_" + tail + changed += 1 + return changed + + +# Cevirici uretimi kimliklerin izi. 9Router chat.completions'a cevirdigi icin +# uydurdugu kimliklerde bu parca bulunur; chatgpt.com boyle bir kaydi saklamaz. +FOREIGN_ID_MARK = "chatcmpl-" + +# Icerigini tasimayan, yalnizca hedefteki kayda ISARET EDEN oge tipleri. +# Kimlik yabanciysa hedef cozemez; ceviriyle kurtarilamaz, atilmalari gerekir. +REFERENCE_TYPES = ("reasoning", "item_reference") + +# Icerigini kendisi tasiyan tipler: yabanci kimlik tasisalar da sorun degil, +# uyari uretmezler. ID_PREFIX'teki tipler de bu gruba dahildir. +INLINE_TYPES = ("message",) + +def drop_foreign_items(req): + """Baska bir saglayicinin urettigi, hedefin cozemeyecegi ogeleri atar. + + Ilke hermes-agent'tan: sahipli kimligi tasima. hermes gecmisi saglayicidan + bagimsiz tuttugu icin model degistirince hicbir sey kirilmiyor; Codex ise + Responses bicimindeki sahipli kimlikleri tasiyor. Model degistirildiginde + chatgpt.com 404 'Item ... not found. Items are not persisted when store is + set to false' diyor ve tur hic baslamiyordu. + + Icerigini iceren ogeler (function_call vb.) atilmaz; onlarin kimligi + fix_tool_ids ile duzeltilir. Yalnizca isaret eden tipler atilir.""" + inp = req.get("input") + if not isinstance(inp, list): + return 0 + kalan, atilan, bilinmeyen = [], 0, set() + for it in inp: + i = it.get("id") if isinstance(it, dict) else None + if isinstance(i, str) and FOREIGN_ID_MARK in i: + ty = it.get("type") + if ty in REFERENCE_TYPES: + atilan += 1 + continue + if ty not in ID_PREFIX and ty not in INLINE_TYPES: + # Tanimadigimiz bir tip yabanci kimlik tasiyor. Simdilik + # gecirilir; hata verirse gunlukte hangi tip oldugu yazili olur. + bilinmeyen.add(ty) + kalan.append(it) + if atilan: + req["input"] = kalan + if bilinmeyen: + log(f"UYARI: yabanci kimlikli taninmayan oge tipleri gecirildi: {sorted(bilinmeyen)}") + return atilan + + +ARAC_OGELERI = {"function_call", "custom_tool_call", "local_shell_call", "tool_search_call", "web_search_call", + "mcp_call", "computer_call", "image_generation_call", "code_interpreter_call"} + +def asistan_mesaji_mi(item): + return isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "assistant" + +def phase_damgala(cikti): + """Bitmis yanitin cikti listesinde phase'i olmayan asistan mesajlarini damgalar. Kural: yanitta arac + cagrisi varsa tur suruyor demektir, butun mesajlar ara aciklamadir; yoksa yalniz SON mesaj son cevaptir. + Codex masaustu ara mesaji bu isaretle tanir; isaretsiz her mesaji son cevap sayar (10 Ekim 2026).""" + arac_var = any(isinstance(o, dict) and o.get("type") in ARAC_OGELERI for o in cikti) + mesajlar = [o for o in cikti if asistan_mesaji_mi(o)] + for o in mesajlar: + if not o.get("phase"): + o["phase"] = "commentary" if (arac_var or o is not mesajlar[-1]) else "final_answer" + +class SseRewriter: + """function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE. + Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne + geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti -> + final_answer). Metin parcalari (delta) bekletilmez, canli akar.""" + def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False): + self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor + self.custom = custom + self.buf = b"" + self.custom_ids = set() + self.bekleyen = None # phase karari bekleyen "output_item.done" olayi + self.arac_goruldu = False + self.hedef = hedef # gecit model kimligi (tasma siniri ogrenmek icin) + self.tasma = tasma # callable(hedef, hata metni): sinir ogrenildiyse cagrilir + self.aracsiz = aracsiz # callable(hedef): hedef "arac cagrisi desteklenmiyor" dedi + def _birak(self, phase): + d, self.bekleyen = self.bekleyen, None + if d is not None and not d["item"].get("phase"): + d["item"]["phase"] = phase + return d + def _gorunur_hata(self, mesaj): + """Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026) + hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda + kullanici hatayi duyar.""" + damga = "%d" % int(time.time() * 1000) + rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga + oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer", + "content": [{"type": "output_text", "text": mesaj, "annotations": []}]} + kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0, + "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0} + return [ + {"type": "response.output_item.added", "output_index": 0, + "item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}}, + {"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj}, + {"type": "response.output_item.done", "output_index": 0, "item": oge}, + {"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed", + "output": [oge], "usage": kullanim}}, + ] + + def _sirala(self, d): + """Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir).""" + t = d.get("type", "") + if t == "error" and not isinstance(d.get("error"), dict) and d.get("message"): + # Gecidin duz bicimi: {"type":"error","code":"bad_request","message":"[400]: ...","status_code":400} + d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""), + "status_code": d.get("status_code")}} + if t == "error" and isinstance(d.get("error"), dict): + cevrilen = self._hata_cevir(d) + if cevrilen is not None: + # Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir. + once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else [] + return once + cevrilen + return self._sirala_normal(d, t) + + def _hata_cevir(self, d): + """Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir.""" + # Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz, + # 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in + # tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin. + metin = str(d["error"].get("message") or d["error"].get("type") or "") + if pencere.aracsiz_mi(metin): + if self.aracsiz is not None: + self.aracsiz(self.hedef) + return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.") + if self.dolu and pencere.kapali_hata_mi(metin): + # Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma + # yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil). + return [{"type": "response.failed", "response": { + "id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], + "error": {"code": "context_length_exceeded", "message": + "Geçit isteği reddetti ve nedenini bildirmedi. Sohbet seçili modelin bağlam penceresine çok yakın; " + "sığmamış olabilir. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}] + if pencere.tasma_mi(metin): + if self.tasma is not None: + self.tasma(self.hedef, metin) + return [{"type": "response.failed", "response": { + "id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], + "error": {"code": "context_length_exceeded", "message": metin[:300]}}}] + if re.search(r"(?" olarak. Soylenen ad + Codex listesinde yoksa buraya bakilir (olcum 11 Ekim 2026: "Seed 2.1 Turbo" gecitte vardi, listede yoktu). + 10 dakika bellekte tutulur; alinamazsa bos liste.""" + simdi = time.time() + if simdi - _GECIT["zaman"] < 600: + return _GECIT["liste"] + _GECIT["zaman"] = simdi + rota = ROUTES.get("aiturk-ide") + if not rota: + _GECIT["liste"] = [] + return [] + try: + adres = rota["url"].rsplit("/", 1)[0] + "/models" + istek = urllib.request.Request(adres, headers={"Authorization": f"Bearer {rota['key']}", "User-Agent": UA}) + with urllib.request.urlopen(istek, timeout=6) as yanit: + veri = json.loads(yanit.read()) + _GECIT["liste"] = [("ide/" + m["id"], m["id"]) for m in veri.get("data", []) if isinstance(m, dict) and m.get("id")] + except Exception as e: + log(f"gecit model listesi alinamadi: {type(e).__name__}: {e}") + _GECIT["liste"] = [] + return _GECIT["liste"] + +def model_adi(slug): + return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug + +class H(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + def log_message(self, *a): pass + + def _read(self): + if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower(): + out = bytearray() + while True: + raw = self.rfile.readline() + if not raw: + break # baglanti kapandi (EOF): sonsuz donguye girme + line = raw.strip() + if not line: + continue + size = int(line.split(b";")[0], 16) + if size == 0: + while self.rfile.readline().strip(): + pass + break + out += self.rfile.read(size) + 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 _stream(self, resp, status, tag, rewriter=None, izle=None): + self.send_response(status) + for k, v in resp.headers.items(): + if k.lower() in RESP_HOP: continue + self.send_header(k, v) + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + total = 0 + bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir + def w(b): + if b: + self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush() + try: + while True: + try: + # read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu. + chunk = resp.read1(4096) + except (OSError, http.client.HTTPException) as e: + # Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex + # yarim akisi tam sanmasin ve yeniden denesin. + log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}") + self.close_connection = True + return + if not chunk: break + total += len(chunk) + if len(bas) < 600: + bas += chunk[:600 - len(bas)] + if izle is not None: + izle.feed(chunk) + w(rewriter.feed(chunk) if rewriter else chunk) + if rewriter: w(rewriter.flush()) + self.wfile.write(b"0\r\n\r\n"); self.wfile.flush() + except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError): + log(f"{tag} istemci baglantiyi kapatti ({total} bayt)") + # Yanit bitmeden istemci kapatti (durdurma ya da uygulama kapanmasi): yarim metin sonraki tura tasinir. + if izle is not None and yarim.kaydet(izle): + log(f"{tag} yarim cevap saklandi ({len(izle.metin)} karakter); ayni sohbetin sonraki istegine not olarak eklenecek") + log(f"{tag} bitti status={status} {total} bayt") + if rewriter is not None and total < 600: + log(f"{tag} kisa akis govdesi: {bas[:500]!r}") + + def _tasma_ogren(self, hedef, metin): + sinir = pencere.sinir_oku(metin) + if sinir and pencere.ogren(HOME, hedef, sinir): + log(f"pencere ogrenildi: {hedef} gercek siniri {sinir} token; model listesi yenileniyor") + threading.Thread(target=load_ide_catalog, daemon=True).start() + + def _aracsiz_ogren(self, hedef): + if pencere.ogren_aracsiz(HOME, hedef): + log(f"aracsiz model ogrenildi: {hedef}; model listesinde isaretlenecek") + threading.Thread(target=load_ide_catalog, daemon=True).start() + + def _send_sse_failed(self, kod, mesaj): + """Codex'in akis icinde tanidigi hata bicimi (response.failed + error.code).""" + kimlik = "resp_astrabridge_%d" % int(time.time() * 1000) + olay = {"type": "response.failed", "response": {"id": kimlik, "object": "response", "status": "failed", + "error": {"code": kod, "message": mesaj}, "output": []}} + govde = b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n" + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(govde))) + self.end_headers() + self.wfile.write(govde) + + def _send_json(self, status, obj): + b = json.dumps(obj).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(b))) + self.end_headers() + self.wfile.write(b) + + def _control(self, path): + """Local status/stop for AITURK IDE. Loopback only; carries no secrets.""" + if self.command == "POST" and path.rstrip("/").endswith("/refresh"): + # The IDE asks for this after a sign-in or a settings change, so the model + # list does not wait for the next periodic refresh. + try: + load_ide_catalog() + except Exception as e: + log(f"katalog yenileme hatasi: {e}") + path = "/aiturk/codex-baglama/status" + self.command = "GET" + if self.command == "GET" and path.rstrip("/").endswith("/status"): + durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ") + return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": durum, + "ide_model": int((sayi.split() or ["0"])[0]), + "ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)), + "is_hatti": bool(HAT_KOMUT[0])}) + if self.command == "POST" and path.rstrip("/").endswith("/stop"): + self._send_json(200, {"ok": True}) + log("AITURK IDE istegiyle kapatiliyor") + threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start() + return + return self._send_json(404, {"error": {"message": "unknown control path"}}) + + def _handle(self): + with lock: + seq[0] += 1; n = seq[0] + if "websocket" in (self.headers.get("Upgrade") or "").lower(): + log(f"[{n} ws-upgrade {self.path[:60]}] -> 426, HTTP'ye dusuruluyor") + self.send_response(426) + self.send_header("Content-Length", "0") + self.send_header("Connection", "close") + self.end_headers() + return + body = self._read() + path = self.path + if path.startswith("/aiturk/codex-baglama/"): + return self._control(path) + if DEBUG: + log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}") + if self.command == "POST" and path.split("?", 1)[0].rstrip("/").endswith("/responses"): + return self._responses(n, path, body) + return self._passthrough(n, path, body) + + def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None, izle=None): + tag = f"[{n} {tagname} {self.command} {path[:80]}]" + hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP} + if rewritten: + hdrs = {k: v for k, v in hdrs.items() if k.lower() != "content-encoding"} + if self.command == "GET" and "/models" in path: + # The upstream validator knows nothing about the added 9Router catalog. + hdrs = {k: v for k, v in hdrs.items() if k.lower() not in ("if-none-match", "if-modified-since")} + req = urllib.request.Request(CHATGPT + path, data=body or None, headers=hdrs, method=self.command) + try: + resp = urllib.request.urlopen(req, timeout=UPSTREAM_IDLE_TIMEOUT); status = resp.status + except urllib.error.HTTPError as e: + resp = e; status = e.code + except Exception as e: + log(f"{tag} HATA {e}") + # Ag yokken de model secici bosalmasin: chatgpt.com'a hic ulasilamazsa son basarili liste verilir. + if self.command == "GET" and "/models" in path and self._son_listeyi_ver(tag, self.headers.get("chatgpt-account-id") or "-"): + return + return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}}) + if self.command == "GET" and "/models" in path: + acct = self.headers.get("chatgpt-account-id") or "-" + if status == 200: + out = self._models(resp, tag) + return out + body = resp.read()[:300] + log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}") + if self._son_listeyi_ver(tag, acct): + return + self.send_response(status) + self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body))) + self.end_headers(); self.wfile.write(body); return + if status >= 400: + errbody = resp.read() + log(f"{tag} chatgpt.com {status}: {errbody[:300]!r}") + self.send_response(status) + for k, v in resp.headers.items(): + if k.lower() in RESP_HOP: continue + self.send_header(k, v) + self.send_header("Content-Length", str(len(errbody))) + self.end_headers(); self.wfile.write(errbody); return + if (self.command == "POST" and "/realtime/calls" in path and status in (200, 201) + and (CFG.get("voice_prep") or {}).get("enabled")): + # Sesli on hazirlayici (bridge.json voice_prep.enabled). Hata atmaz; basliklar yalniz bellekte. + try: + import voice_prep + if voice_prep.baslat(resp.headers.get("Location"), hdrs, os.path.join(HOME, "bridge.json"), LOGDIR): + log(f"{tag} sesli on hazirlayici baslatildi") + except Exception as e: + log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}") + self._stream(resp, status, tag, rewriter, izle) + + def _son_listeyi_ver(self, tag, acct): + """Bu hesabin son basarili model listesini gonderir. True: gonderildi.""" + cached = LAST_MODELS.get(acct) + if not cached: + return False + self.send_response(200) + for k, v in cached["headers"]: + self.send_header(k, v) + self.send_header("Content-Length", str(len(cached["body"]))) + self.end_headers(); self.wfile.write(cached["body"]) + log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})") + return True + + def _openai_ozet_coz(self, n, oge, ozet_cfg): + """OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz).""" + klasor = os.path.join(LOGDIR, "ozet-onbellek") + metin = ozet_kopru.onbellekten(klasor, oge) + if metin: + return metin + govde = ozet_kopru.coz_istegi(oge, ozet_cfg.get("openai_modeli", "gpt-6-luna")) + hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"} + hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream" + r = urllib.request.Request(CHATGPT + self.path, data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST") + try: + resp = urllib.request.urlopen(r, timeout=float(ozet_cfg.get("coz_zaman_asimi", 300))) + metin = ozet_kopru.sse_metni(resp.read()) + except urllib.error.HTTPError as e: + log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: HTTP {e.code} {e.read()[:200]!r}") + return None + except Exception as e: + log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: {type(e).__name__}") + return None + if metin: + ozet_kopru.onbellege(klasor, oge, metin) + log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)") + return metin or None + + def _ozet_openai(self, n, path, req, slug, ozet_cfg, girdi_bayt): + """Kopru modeline (ide/, 9r/) gelen sikistirma isteginin ozetini abonelik modeline yazdirir. + Donus: True = yanit istemciye gitti; False = abonelik istegi baslamadan basarisiz oldu + (istemciye hicbir sey yazilmadi, cagiran hedef modelle devam eder). req cagiranin kopyasidir.""" + model = ozet_cfg.get("ozet_modeli", "gpt-6.1-sol") + tag = f"[{n} CHATGPT {model} <- {slug}]" + try: + req["model"] = model + fix_tool_ids(req) + atilan = drop_foreign_items(req) + ozet_kopru.ozetleri_ac(req, yabanci_hedef=False) + ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem")) + body = json.dumps(req, ensure_ascii=False).encode("utf-8") + except Exception as e: + log(f"{tag} ozet kopru: abonelik ozeti hazirlanamadi ({type(e).__name__}: {e}), hedef model deneniyor") + return False + hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"} + hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream" + r = urllib.request.Request(CHATGPT + path, data=body, headers=hdrs, method="POST") + try: + resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT) + except urllib.error.HTTPError as e: + log(f"{tag} ozet kopru: abonelik ozeti reddedildi (HTTP {e.code} {e.read()[:200]!r}), hedef model deneniyor") + return False + except Exception as e: + log(f"{tag} ozet kopru: abonelik ozeti baslatilamadi ({type(e).__name__}), hedef model deneniyor") + return False + log(f"{tag} ozet kopru: sikistirma ozeti abonelik modeline yazdiriliyor (girdi={len(req.get('input', []))} oge, " + f"{girdi_bayt} bayt, {atilan} yabanci oge atildi)") + akis = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg)) + self._stream(resp, resp.status, tag, akis) + log(f"{tag} ozet kopru: {akis.sonuc_notu()}") + return True + + def _models(self, resp, tag): + raw = resp.read() + try: + raw = decompress(raw, resp.headers.get("Content-Encoding")) + data = json.loads(raw) + models = data.get("models", []) + changed = 0 + # Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz). + by_slug = {m.get("slug"): m for m in models} + ozgun_kimlikler = set(by_slug) + ekler = EXTRA_MODELS # tek kopya: liste hazirlanirken katalog yenilenirse sira ve icerik tutarli kalir + for x in ekler: + if x["slug"] in by_slug: + continue + base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {}) + m = copy.deepcopy(base) + m["slug"] = x["slug"] + m["display_name"] = x.get("display_name", x["slug"]) + m["description"] = x.get("description", "") + m["visibility"] = "list" + m["priority"] = x.get("priority", 0) + m["upgrade"] = None; m["availability_nux"] = None + dis = x["slug"].startswith(IDE_PREFIX) + if not dis: + m["additional_speed_tiers"] = []; m["service_tiers"] = [] + # Dis modelde hiz katmani ilan edilmeye devam eder (yoksa Codex her turda "service tier ... will be + # omitted" uyarisi basar); istekteki service_tier alani gecide gonderilmeden once silinir. + if dis and CFG.get("dis_arac_modu", "direct") == "direct": + # Abonelik kalibi "code_mode_only": tek arac JavaScript calistiran `exec`. Zayif modeller bunu + # cagiramiyor ya da ciktiyi yazdirmiyor (10 Ekim 2026 olcumu). Dis modellere Codex'in klasik + # araclari dogrudan verilir (GPT-5.5 kalibindaki gibi); Responses Lite kapatilir. + m["tool_mode"] = None + m["use_responses_lite"] = False + m["experimental_supported_tools"] = [] + if x.get("context_window"): + cw = int(x["context_window"]) + m["context_window"] = cw; m["max_context_window"] = cw + if x.get("reasoning_levels"): + m["supported_reasoning_levels"] = [l for l in m.get("supported_reasoning_levels", []) if l.get("effort") in x["reasoning_levels"]] + if SLIM_CLONES: + saved = {f_: m[f_] for f_ in SLIM_FIELDS if f_ in m} + for f_ in SLIM_FIELDS: + m.pop(f_, None) + # Codex, bir girdide hem base_instructions hem + # model_messages.instructions_template yoksa listenin TAMAMINI + # reddeder. Kirpma sonrasi ikisi de kalmadiysa geri koy. + mm = m.get("model_messages") + has_tmpl = isinstance(mm, dict) and mm.get("instructions_template") + if not m.get("base_instructions") and not has_tmpl: + m.update(saved) + models.append(m); changed += 1 + if ekler: + ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler) + out = json.dumps(data, ensure_ascii=False).encode("utf-8") + except Exception as e: + log(f"{tag} model listesi degistirilemedi: {e}") + out = raw; changed = 0 + raw_len = len(out) + duz = out # sikistirilmamis govde: yedege bu yazilir + catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"' + enc_out = None + if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed: + import gzip + out = gzip.compress(out, compresslevel=6); enc_out = "gzip" + hdrs_out = [] + for k, v in resp.headers.items(): + kl = k.lower() + if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue + if kl in ("etag", "x-models-etag") and changed: + v = catalog_etag + hdrs_out.append((k, v)) + if changed: + # Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de). + acct_ = self.headers.get("chatgpt-account-id") or "-" + onceki = LAST_MODELS.get(acct_) + LAST_MODELS[acct_] = {"headers": list(hdrs_out), "body": duz, "at": time.strftime("%Y-%m-%d %H:%M:%S")} + # Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz. + if not onceki or onceki.get("body") != duz: + save_last_models(acct_, list(hdrs_out), duz) + if enc_out: + hdrs_out.append(("Content-Encoding", enc_out)) + self.send_response(200) + for k, v in hdrs_out: + self.send_header(k, v) + self.send_header("Content-Length", str(len(out))) + self.end_headers() + self.wfile.write(out) + log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi") + + def _yerel_yanit(self, slug, metin): + govde = hazir_yaniti(slug, metin) + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Content-Length", str(len(govde))) + self.end_headers() + self.wfile.write(govde) + + def _model_komutu(self, n, req, sohbet, gelen): + """Son kullanici mesaji "modeli X yap" / "hangi model" ise isler. Yerel cevap metni doner; + None: istek (gerekirse degistirilmis olarak) modele gider.""" + metin, sira, sesli = model_gecis.son_kullanici_metni(req) + komut = model_gecis.komut_coz(metin) if metin else None + if not komut: + return None + etkin = yonlendirilen(sohbet, gelen) or gelen + ortada = model_gecis.tur_ortasi(req, sira) + if komut["tur"] == "sor": + if ortada: + return None + log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}") + return f"Sohbetin modeli şu an {model_adi(etkin)}." + if komut["tur"] == "nasil": + if ortada: + return None + log(f"[{n} {gelen}] adsiz model komutu: nasil soylenecegi anlatildi") + return f"Sohbetin modeli şu an {model_adi(etkin)}. " + model_gecis.NASIL + # Sesle gecis yalniz bizim ekledigimiz modeller arasindadir; Codex'in kendi modelleri aday degildir + # (kullanici karari, 11 Ekim 2026). Iki taraf arasinda gecis: ekrandaki seciciden model secilir, "." yazilir. + bilinen = bilinen_modeller() + sonuc = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if ek_model_mu(s)]) + if not sonuc: + sonuc = model_gecis.eslestir(komut["ad"], gecit_modelleri()) # Codex listesinde olmayan gecit modeli + if not sonuc: + ozgun = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if not ek_model_mu(s)]) + if ozgun and (komut.get("kesin", True) or "slug" in ozgun): + kim = f"{ozgun['ad']} Codex'in kendi modeli" if "ad" in ozgun else "Bu ad Codex'in kendi modellerine uyuyor" + ileti = (f"{kim}; Codex'in kendi modellerine sesle geçilmiyor, model değişmedi. Geçmek için ekrandaki " + f"model seçiciden o modeli seç ve tek başına \".\" yaz. Sohbetin modeli hâlâ {model_adi(etkin)}.") + log(f"[{n} {gelen}] model komutu: Codex'in kendi modeli istendi {komut['ad']}, gecilmedi") + if not ortada: + return ileti + if not komut.get("kesin", True): + return None + model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı Codex'in kendi modeline geçmek istedi; bu sesle " + "yapılamıyor, model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + ileti + "]") + self.komut_govdeyi_degistirdi = True + return None + if not komut.get("kesin", True) and not (sonuc and "slug" in sonuc): + return None # "modeli" denmemis ve ad tek modele oturmuyor: komut degil, modele gider + if not sonuc: + if ortada: + return None + # Acik komut ("modeli ... yap") ama ad bulunamadi: model "kopru yapacak" diye yanlis soz vermesin. + log(f"[{n} {gelen}] model komutu: ad bulunamadi {komut['ad']}") + return (f"\"{' '.join(komut['ad'])}\" adına uyan bir model bulamadım, model değişmedi; sohbetin modeli hâlâ " + f"{model_adi(etkin)}. Adını biraz farklı söyler misin?") + if "adaylar" in sonuc: + soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: " + + ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?") + log(f"[{n} {gelen}] model komutu belirsiz: {komut['ad']} -> {sonuc['adaylar']}") + if not ortada: + return soru + model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; " + "model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]") + self.komut_govdeyi_degistirdi = True + return None + hedef = sonuc["slug"] + if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES: + return "AITURK bağlantısı şu an kapalı; bu modele geçemedim, model değişmedi." + YONLENDIRME.koy(sohbet, gelen, hedef) + log(f"[{n} {gelen}] model komutu: {model_adi(etkin)} -> {sonuc['ad']}" + f" ({'tur ortasi, is devrediliyor' if ortada else 'bosta'}{', sesli' if sesli else ''})") + if hedef == etkin: + return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}." + if ortada: + model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin))) + self.komut_govdeyi_degistirdi = True + return None + # Listede ayni modeli yeniden secmek yonlendirmeyi kaldirmaz (Codex ayni adi gonderir); geri donus yolu soylenir. + donus = f" Listede seçili modele dönmek için \"modeli {model_adi(gelen)} yap\" de." if hedef != gelen else "" + return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. " + f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}") + + def _ozgun(self, n, path, body, req, slug): + """Codex'in kendi (abonelik) modeli: kopru HICBIR SEY yapmaz, istek chatgpt.com'a bayt bayt oldugu gibi gider. + Kullanici karari (11 Ekim 2026): "Codex kendi modelini kullanirken kopruyle bir bagi olmamali ... Codex'in + kendi modeline dokunulmamali" ve "Codex'i secip Codex'e yaziyorsan bizim kopru yardim amacli bile yaninda + olamaz." Bu yuzden model komutu, nokta karsilama, bilgi notu, is hatti talimati, yarim cevap notu, + sikistirmaya karisma ve koprunun kendi izlerini temizleme de YOKTUR; sikistirmayi Codex kendisi yapar + (olcum 11 Ekim 2026: kopru ozgun modelde ozeti kendi uretmeye calisip Codex'i "Error running remote compact + task" ile takti). + Bilinen sonuc: ayni sohbette daha once ek model kullanildiysa ya da kopru ozeti kaldiysa chatgpt.com bu + istegi reddedebilir; o sohbet ek modellerle surer, Codex'in kendi modeli icin yeni sohbet acilir.""" + return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=False) + + def _responses(self, n, path, body): + try: + req = json.loads(decompress(body, self.headers.get("Content-Encoding"))) + except Exception as e: + log(f"[{n} responses] govde cozulemedi: {e}") + return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}}) + slug = req.get("model", "") + gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model) + if not ek_model_mu(gelen): + return self._ozgun(n, path, body, req, slug) + govde_degisti = False # istek govdesi kopruce degistirildi: abonelik yolunda yeniden yazilir + once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True) + sohbet = str(req.get("prompt_cache_key") or "") + if CFG.get("sesle_model", True) and sohbet: + self.komut_govdeyi_degistirdi = False + try: + yerel = self._model_komutu(n, req, sohbet, gelen) + except Exception as e: + yerel = None + log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}") + if yerel is not None: + return self._yerel_yanit(gelen, yerel) + if self.komut_govdeyi_degistirdi: + govde_degisti = True # komut mesaji nota cevrildi: abonelik yolunda da degismis govde gider + try: + if model_gecis.gecmisi_temizle(req, bilinen_modeller(), yonlendirilen(sohbet, gelen) or gelen): + govde_degisti = True + except Exception as e: + log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}") + hedef = yonlendirilen(sohbet, gelen) + if hedef and hedef != gelen: + req["model"] = slug = hedef + log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor") + mapping = MODEL_MAP.get(slug) + if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])): + # Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida + # keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar. + del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir + govde_degisti = True + log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi") + if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)): + govde_degisti = True + if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req): + # Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da + # istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur. + log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)") + return self._yerel_yanit(gelen, f"{model_adi(slug)} seçildi. Sohbetin modeli artık {model_adi(slug)}.") + # Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py). + kaynak = self.headers.get("originator") or "" + if kaynak and kaynak not in GORULEN_ISTEMCI: + GORULEN_ISTEMCI.add(kaynak) + log(f"istemci goruldu: {kaynak}") + try: + # Model, sohbetin modelinin soylenerek degistirilebildigini bilsin; yoksa "degistiremem" diyor. + if CFG.get("sesle_model", True) and CFG.get("model_bilgi", True) and sohbet and model_gecis.bilgi_ekle(req): + govde_degisti = True + if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle( + req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)): + govde_degisti = True + except Exception as e: + log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}") + # Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir. + yarim_eklenen = 0 + if CFG.get("yarim_cevap", True): + try: + yarim_eklenen = yarim.uygula(req, sohbet) + except Exception as e: + log(f"[{n} {slug}] yarim cevap notu atlandi: {type(e).__name__}: {e}") + if yarim_eklenen: + log(f"[{n} {slug}] yarim cevap notu eklendi ({yarim_eklenen} karakter)") + izle = yarim.Izleyici(sohbet, slug) if (sohbet and CFG.get("yarim_cevap", True)) else None + try: + if not ozet_kopru.tetik_mi(req): + OZET_FRENI.normal(sohbet or slug) # ozet olmayan istek: art arda ozet sayaci sifirlanir + except Exception: + pass + ozet_cfg = CFG.get("ozet_kopru") or {} + ozet_acik = ozet_cfg.get("enabled", False) + catalog = CFG.get("nine_router_catalog") or {} + prefix = catalog.get("prefix", "9r/") + if mapping is None and prefix and slug.startswith(prefix): + if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES: + # Newly added API models can be selected before the background refresh. + # Provider-qualified IDs are passed intact to 9Router, never to ChatGPT. + mapping = (catalog.get("route", "9router"), slug[len(prefix):], None) + else: + # Rotasi kalmamis eski ad chatgpt.com'a baska bir adla gonderilmez ("model desteklenmiyor" donuyordu). + log(f"[{n} {slug}] bu on ekin rotasi yok; istek gonderilmedi") + return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}}) + ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler + if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES: + mapping = (ide_route, slug[len(ide_prefix):], None) + if mapping is None and slug.startswith(ide_prefix): + # Never send an IDE model to chatgpt.com under another name. + log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok") + # 503 donmek Codex'i sessizce yeniden denetiyor ve tur cevapsiz bitiyordu; sesli asistan da "bakiyorum" + # deyip duruyordu (olcum 11 Ekim 2026, sunucu gecisi sirasinda). Kullanici durumu duysun. + if CFG.get("kesinti_mesaji", True): + return self._yerel_yanit(gelen, "AITURK modellerine şu an ulaşılamıyor (AITURK sunucusu cevap vermiyor ya da " + "bağlantı kapalı); bu model şimdi cevap veremez. Beklemek istemezsen ekrandaki model " + "seçiciden Codex'in kendi modellerinden birini seç ve tek başına \".\" yaz.") + return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}}) + if mapping is None: + # Codex'in kendi modelleri en basta _ozgun ile ayrildi; buraya yalniz rotasi cozulemeyen bir ek ad duser. + # Ek model chatgpt.com'a baska bir adla gonderilmez. + log(f"[{n} {slug}] ek modelin rotasi cozulemedi; istek gonderilmedi") + return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}}) + route_name, target, flat = mapping + try: + ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req) + except Exception as e: + ozet_tetik = False; ozet_acik = False + log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}") + girdi_bayt = 0 + if ozet_tetik: + OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3)) + if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()): + log(f"[{n} {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)") + return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir, + "type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}}) + girdi_bayt = len(json.dumps(req.get("input") or [], ensure_ascii=False).encode("utf-8")) + if ozet_tetik and ozet_cfg.get("model"): + # Ozeti baglami alabilen baska bir modele yazdirmak icin (ornegin kucuk hedef kendi basina ozetleyemiyorsa). + alt = MODEL_MAP.get(ozet_cfg["model"]) + if alt: + route_name, target, flat = alt + elif ozet_tetik and ozet_cfg.get("ozetleyen", "openai") == "openai": + # Ozeti secili kopru modeli degil, abonelikteki guclu model yazar (10 Ekim 2026: 1 MB'lik + # sohbeti kucuk bir model 2201 karaktere indirdi ve model degisince hafiza gitti). + # Abonelik istegi baslamadan reddedilirse asagidaki eski yol (hedef model) calisir. + if self._ozet_openai(n, path, copy.deepcopy(req), slug, ozet_cfg, girdi_bayt): + return + # 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir. + via_router = route_name in (catalog.get("route", "9router"), ide_route) + route = ROUTES.get(route_name) + if route is None: + # Katalog yenilemesi rotayi bu istek hazirlanirken kaldirdi (baglama kapandi ya da IDE oturumu gitti). + log(f"[{n} {slug}] rota artik yok: {route_name}") + return self._send_json(503, {"error": {"message": "Bu modelin baglantisi su an kapali; birazdan yeniden deneyin ya da baska bir model secin."}}) + tag = f"[{n} {route_name.upper()} {slug}->{target}]" + req["model"] = target + req.pop("client_metadata", None) + if route_name == ide_route: + req.pop("service_tier", None) # abonelik hiz katmani; gecit tanimaz + for k in route.get("strip_fields", []): + req.pop(k, None) + if ozet_acik: + ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True, coz=lambda oge: self._openai_ozet_coz(n, oge, ozet_cfg)) + if ob["acilan"] or ob["cozulen"] or ob["okunamayan"]: + log(f"{tag} ozet kopru: kopru ozeti={ob['acilan']}, OpenAI ozeti duz metne cevrildi={ob['cozulen']}, okunamayan={ob['okunamayan']}") + if ozet_tetik: + ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem")) + log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)") + if via_router and CFG.get('resim_sadelestir', True): + # Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026). + # Duzlestirmeden ONCE: kimliksiz arac ciktisi orada duz metne cevrilir ve icindeki resim taninmaz olur. + cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel"))) + if cikan: + log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)") + rewriter = None + if flat if flat is not None else route.get("flatten_tools"): + custom = flatten_request(req) + duzelen = arac_semalarini_duzelt(req) + if duzelen: + log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)") + rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren) + fix_tool_ids(req) + if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'): + merged = coalesce_context(req) + if merged: + log(f"{tag} Gemini instruction context: {merged} message(s) preserved") + elif via_router and CFG.get('pano_kanca_user', True): + # 9Router Claude cevirisi developer mesajlarini dusuruyor; bu turun PANO OLAYLARI blogu (sesli_pano.py) + # sona user mesaji olarak eklenir (kanca_context.py, 24 Eylul 2026). bridge.json "pano_kanca_user": false kapatir. + eklenen, ozet = pano_blogunu_ulastir(req) + if eklenen: + log(f"{tag} PANO OLAYLARI blogu user mesaji olarak eklendi ({eklenen} karakter, {ozet})") + if via_router: + # Claude "assistant message prefill" desteklemiyor (25 Eylul 2026, sesli tur kapaninca 400). + eklendi, sonlar = claude_sonu_duzelt(req) + if eklendi: + log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi") + hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere") + # Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz. + # Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir). + pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere) + if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik: + kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude")))) + if rewriter is not None: + rewriter.dolu = tahmin >= hedef_pencere * 0.7 + if kirpilan: + log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})") + if not sigdi: + log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})") + return self._send_sse_failed("context_length_exceeded", + f"Bu mesaj seçili modelin bağlam penceresine sığmıyor (yaklaşık {tahmin} token, pencere {hedef_pencere}). " + "Daha büyük pencereli bir model seçin ya da mesajı kısaltın.") + data = json.dumps(req, ensure_ascii=False).encode("utf-8") + log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt") + if DEBUG: + open(os.path.join(LOGDIR, f"{n:04d}_req.json"), "wb").write(data) + hdrs = {"Authorization": f"Bearer {route['key']}", "Content-Type": "application/json", "Accept": "text/event-stream", "User-Agent": UA} + last = None + # A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may + # have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026); + # only failures before the work starts (no connection, 500/502/503/529) are retried. + combo = "/" not in target + for attempt in range(1, RETRIES + 1): + r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST") + try: + resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT) + except urllib.error.HTTPError as e: + errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir + last = (e.code, errbody[:400]) + etxt = errbody.decode("utf-8", "replace") + # 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak + # donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu. + permanent = (any(p.lower() in etxt.lower() for p in NO_RETRY_PATTERNS) + or bool(re.search(r"\[4(?!29|08)\d\d\]", etxt))) + if permanent: + log(f"{tag} kalici hata, tekrar denenmiyor: HTTP {e.code} {errbody[:160]!r}") + # 524 = Cloudflare gave up waiting while the origin kept working; a retry + # re-runs (and re-bills) the whole Fusion (22 Sep 2026: 3 double runs). + elif e.code in ((500, 502, 503, 529) if combo else (500, 502, 503, 504, 529)) and attempt < RETRIES: + log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar") + time.sleep(min(2 * attempt, 8)); continue + log(f"{tag} HTTP {e.code} {errbody[:200]!r}") + if pencere.tasma_mi(etxt): + # Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin + # bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur). + self._tasma_ogren(target, etxt) + return self._send_sse_failed("context_length_exceeded", etxt[:300]) + self.send_response(e.code) + self.send_header("Content-Type", "application/json") + bekle = e.headers.get("Retry-After") if e.headers is not None else None + if bekle: + self.send_header("Retry-After", str(bekle)) # 429'da ne kadar beklenecegi istemciye ulassin + self.send_header("Content-Length", str(len(errbody))) + self.end_headers(); self.wfile.write(errbody); return + except Exception as e: + last = (0, str(e)) + if combo and is_timeout(e): + log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}") + break + if combo and kopma_mi(e): + log(f"{tag} combo istegi gonderildikten sonra baglanti koptu, tekrar gonderilmiyor (calisiyor olabilir): {e}") + break + log(f"{tag} deneme {attempt}: {e}") + time.sleep(min(2 * attempt, 8)); continue + # Headers go to the client from here on: a cut stream is never resent. + if ozet_tetik: + akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg)) + sonuc = self._stream(resp, resp.status, tag, akis) + log(f"{tag} ozet kopru: {akis.sonuc_notu()}") + return sonuc + return self._stream(resp, resp.status, tag, rewriter, izle) + self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}}) + + do_POST = _handle + do_GET = _handle + do_PUT = _handle + do_DELETE = _handle + do_PATCH = _handle + +class TS(http.server.ThreadingHTTPServer): + daemon_threads = True + # Windows'ta SO_REUSEADDR baska bir surecin ayni portu "calmasina" izin verir. + # Kapali: port doluysa sessizce yan yana dinlemek yerine acikca hata verir. + allow_reuse_address = False + +if __name__ == "__main__": + load_last_models() + log(f"AstraBridge :{PORT} routes={list(ROUTES)} model_map={len(MODEL_MAP)} diger->{CHATGPT}") + try: + srv = TS(("127.0.0.1", PORT), H) + except OSError as e: + log(f"HATA: 127.0.0.1:{PORT} baglanamadi ({e}). Port baska bir surecte olabilir; " + f"bridge.json icindeki 'port' degerini ve config.toml'daki openai_base_url'i degistirin.") + sys.exit(1) + srv.serve_forever() diff --git a/apps/desktop/assets/codex-bridge/test_access_programs.py b/apps/desktop/assets/codex-bridge/test_access_programs.py index d5dda6f..bf57cae 100644 --- a/apps/desktop/assets/codex-bridge/test_access_programs.py +++ b/apps/desktop/assets/codex-bridge/test_access_programs.py @@ -10,7 +10,7 @@ import urllib.request # Load only the actual adapter and routing method: no catalog/network/thread # startup or credential loading is needed to exercise the production code. -source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +source = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) flatten = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "flatten_request") claude_sonu = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt") handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H") diff --git a/apps/desktop/assets/codex-bridge/test_ayirici.py b/apps/desktop/assets/codex-bridge/test_ayirici.py new file mode 100644 index 0000000..a0a9ea2 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_ayirici.py @@ -0,0 +1,214 @@ +"""The splitter (bridge.py): Codex's own models go to chatgpt.com byte for byte, added models to the bridge process. + +Real sockets, no network: a fake chatgpt.com and a fake bridge process record what reaches them.""" +import gzip +import hashlib +import http.client +import http.server +import json +import tempfile +import threading +import time +import unittest + +import bridge + + +class Kayit(http.server.BaseHTTPRequestHandler): + """Records every request; answers from the server's `cevap` function.""" + protocol_version = "HTTP/1.1" + + def log_message(self, *a): + pass + + def _her(self): + n = int(self.headers.get("Content-Length") or 0) + govde = self.rfile.read(n) if n else b"" + self.server.gelen.append({"yontem": self.command, "yol": self.path, "baslik": dict(self.headers), "govde": govde}) + self.server.cevap(self, govde) + + do_GET = do_POST = _her + + +def sunucu(cevap): + s = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Kayit) + s.daemon_threads = True + s.gelen, s.cevap = [], cevap + threading.Thread(target=s.serve_forever, daemon=True).start() + return s + + +def duz(durum, govde, tur="application/json"): + def f(h, _): + h.send_response(durum) + h.send_header("Content-Type", tur) + h.send_header("Content-Length", str(len(govde))) + h.end_headers() + h.wfile.write(govde) + return f + + +def zstd(veri): + try: + from compression import zstd as z + return z.compress(veri) + except ImportError: + import zstandard + return zstandard.ZstdCompressor().compress(veri) + + +class Ayirici(unittest.TestCase): + def setUp(self): + self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream")) + self.kopru = sunucu(duz(200, b'event: response.completed\ndata: {"kopru":1}\n\n', "text/event-stream")) + self.on = bridge.Sunucu(("127.0.0.1", 0), bridge.H) + threading.Thread(target=self.on.serve_forever, daemon=True).start() + self.eski = (bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, dict(bridge.CFG)) + self.gecici = tempfile.TemporaryDirectory() + bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}" + bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}" + bridge.LOGDIR = self.gecici.name + bridge.CFG.clear() + + def tearDown(self): + for s in (self.on, self.ozgun, self.kopru): + s.shutdown() + s.server_close() + bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, cfg = self.eski + bridge.CFG.clear() + bridge.CFG.update(cfg) + self.gecici.cleanup() + + def iste(self, yontem, yol, govde=None, baslik=None): + b = http.client.HTTPConnection("127.0.0.1", self.on.server_address[1], timeout=10) + b.request(yontem, yol, body=govde, headers=baslik or {}) + y = b.getresponse() + veri = y.read() + b.close() + return y.status, veri, dict(y.getheaders()) + + def test_codex_own_model_reaches_chatgpt_byte_for_byte(self): + """Compressed or not, with a command, a lone dot or a compaction trigger in it: nothing is changed.""" + istekler = [{"model": "gpt-6-astra", "input": [{"role": "user", "content": "modeli opus 5 5 yap"}]}, + {"model": "gpt-6-luna", "input": [{"role": "user", "content": "."}]}, + {"model": "gpt-6-astra", "prompt_cache_key": "s", "input": [{"type": "compaction_trigger"}]}, + {"model": "gpt-6-astra", "input": [{"type": "compaction", "encrypted_content": "ASTRABRIDGE-OZET-1:eA=="}, + {"type": "reasoning", "id": "rs_chatcmpl-x"}]}] + for veri in istekler: + ham = json.dumps(veri, ensure_ascii=False).encode("utf-8") + for kodlama, govde in (("zstd", zstd(ham)), ("gzip", gzip.compress(ham)), ("", ham)): + with self.subTest(model=veri["model"], kodlama=kodlama): + baslik = {"Authorization": "Bearer sahte", "originator": "Codex Desktop", "chatgpt-account-id": "h1"} + if kodlama: + baslik["Content-Encoding"] = kodlama + durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", govde, baslik) + giden = self.ozgun.gelen[-1] + self.assertEqual(hashlib.sha256(giden["govde"]).hexdigest(), hashlib.sha256(govde).hexdigest()) + basliklar = {k.lower(): v for k, v in giden["baslik"].items()} + self.assertEqual(basliklar.get("content-encoding", ""), kodlama) + self.assertEqual((basliklar["authorization"], basliklar["originator"], basliklar["chatgpt-account-id"]), + ("Bearer sahte", "Codex Desktop", "h1")) + self.assertEqual(giden["yol"], "/backend-api/codex/responses") + self.assertEqual((durum, cevap), (200, b'event: response.completed\ndata: {"ok":1}\n\n')) + self.assertEqual(self.kopru.gelen, []) # the bridge process never saw any of them + + def test_added_models_go_to_the_bridge_process_and_only_there(self): + for model in ("ide/Opus-5", "9r/cc/claude-opus-5-5"): + govde = zstd(json.dumps({"model": model, "input": []}).encode()) + durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", govde, + {"Content-Encoding": "zstd", "originator": "Codex Desktop"}) + self.assertEqual((durum, cevap), (200, b'event: response.completed\ndata: {"kopru":1}\n\n')) + self.assertEqual(self.kopru.gelen[-1]["govde"], govde) + basliklar = {k.lower(): v for k, v in self.kopru.gelen[-1]["baslik"].items()} + self.assertEqual((basliklar["originator"], basliklar["content-encoding"]), ("Codex Desktop", "zstd")) + bridge.CFG["model_map"] = {"ozel-model": ["rota", "hedef"]} + self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ozel-model"}).encode()) + self.assertEqual(len(self.kopru.gelen), 3) + self.assertEqual(self.ozgun.gelen, []) + + def test_a_body_that_cannot_be_read_takes_codex_own_path_untouched(self): + for govde, baslik in ((b"\x00\x01bozuk", {"Content-Encoding": "zstd"}), (b"json degil", {}), (b"[1,2]", {})): + self.iste("POST", "/backend-api/codex/responses", govde, baslik) + self.assertEqual(self.ozgun.gelen[-1]["govde"], govde) + self.assertEqual(self.kopru.gelen, []) + + def test_codex_own_model_keeps_working_while_the_bridge_process_is_down(self): + self.kopru.shutdown() + self.kopru.server_close() + govde = json.dumps({"model": "gpt-6-astra", "input": []}).encode() + self.assertEqual(self.iste("POST", "/backend-api/codex/responses", govde)[0], 200) + # The model list falls back to chatgpt.com's own list, unchanged. + self.ozgun.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"}]}') + durum, liste, _ = self.iste("GET", "/backend-api/codex/models?client_version=1", None, {"If-None-Match": "x"}) + self.assertEqual((durum, liste), (200, b'{"models":[{"slug":"gpt-6-astra"}]}')) + # An added model says so instead of hanging or going to chatgpt.com under another name. + durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ide/Opus-5"}).encode()) + self.assertEqual(durum, 503) + self.assertIn("köprüsü şu an çalışmıyor", json.loads(cevap)["error"]["message"]) + self.assertEqual([g["yol"].split("?")[0] for g in self.ozgun.gelen], + ["/backend-api/codex/responses", "/backend-api/codex/models"]) + # Status still answers in the shape AITURK IDE expects. + durum, cevap, _ = self.iste("GET", "/aiturk/codex-baglama/status") + d = json.loads(cevap) + self.assertEqual((durum, d["surum"], d["kopru"], d["ayirici"]), (200, "8", "kapali", True)) + + def test_model_list_comes_from_the_bridge_and_falls_back_when_it_fails(self): + self.kopru.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"},{"slug":"ide/Opus-5"}]}') + self.assertIn(b"ide/Opus-5", self.iste("GET", "/backend-api/codex/models")[1]) + self.assertEqual(self.ozgun.gelen, []) + self.kopru.cevap = duz(500, b'{"error":"x"}') + self.ozgun.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"}]}') + self.assertEqual(self.iste("GET", "/backend-api/codex/models")[1], b'{"models":[{"slug":"gpt-6-astra"}]}') + + def test_everything_else_and_errors_pass_through_as_they_are(self): + self.ozgun.cevap = duz(200, b'{"sonuc":1}') + self.assertEqual(self.iste("POST", "/backend-api/codex/alpha/search", b'{"q":"x"}')[1], b'{"sonuc":1}') + self.assertEqual(self.ozgun.gelen[-1]["govde"], b'{"q":"x"}') + self.ozgun.cevap = duz(400, b'{"error":{"message":"gecersiz"}}') + durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "gpt-6-astra"}).encode()) + self.assertEqual((durum, cevap), (400, b'{"error":{"message":"gecersiz"}}')) + self.assertEqual(self.kopru.gelen, []) + + def test_a_long_answer_streams_piece_by_piece_without_waiting_for_the_end(self): + """This morning's failure was an idle stream: Codex got no bytes for five minutes. Bytes must flow as they come.""" + def yavas(h, _): + h.send_response(200) + h.send_header("Content-Type", "text/event-stream") + h.send_header("Transfer-Encoding", "chunked") + h.end_headers() + for parca in (b"data: bir\n\n", b"data: iki\n\n"): + h.wfile.write(b"%x\r\n" % len(parca) + parca + b"\r\n") + h.wfile.flush() + time.sleep(1.0) + h.wfile.write(b"0\r\n\r\n") + self.ozgun.cevap = yavas + b = http.client.HTTPConnection("127.0.0.1", self.on.server_address[1], timeout=10) + bas = time.time() + b.request("POST", "/backend-api/codex/responses", body=json.dumps({"model": "gpt-6-astra"}).encode()) + y = b.getresponse() + ilk = y.read1(64) + ilk_sure = time.time() - bas + kalan = y.read() + b.close() + self.assertEqual(ilk, b"data: bir\n\n") + self.assertLess(ilk_sure, 0.8) # the first piece arrived before the answer ended + self.assertEqual(kalan, b"data: iki\n\n") + + def test_status_is_the_bridge_status_plus_the_splitter_fields(self): + self.kopru.cevap = duz(200, b'{"ok": true, "surum": "8", "kod": "abc", "ide_durum": "tamam", "ide_model": 147}') + d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1]) + self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), ("8", 147, "calisiyor", True)) + + def test_a_persistent_connection_is_refused_the_way_it_is_today(self): + durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"}) + self.assertEqual(durum, 426) + self.assertEqual(self.ozgun.gelen + self.kopru.gelen, []) + + def test_which_side_a_model_is_on(self): + for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False), + ("", False), (None, False), ("idea", False)): + self.assertEqual(bridge.ek_model_mu(slug), ek, slug) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/desktop/assets/codex-bridge/test_claude_sonu.py b/apps/desktop/assets/codex-bridge/test_claude_sonu.py index b5deeda..b7d3b5f 100644 --- a/apps/desktop/assets/codex-bridge/test_claude_sonu.py +++ b/apps/desktop/assets/codex-bridge/test_claude_sonu.py @@ -4,7 +4,7 @@ import json from pathlib import Path import unittest -tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) env = {} exec(compile(ast.Module(body=[n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt"], type_ignores=[]), "bridge.py", "exec"), env) f = env["claude_sonu_duzelt"] diff --git a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py index ab8c9d5..1f79905 100644 --- a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py +++ b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py @@ -13,7 +13,7 @@ from unittest.mock import Mock import urllib.error import urllib.request -source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +source = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) pick = lambda name: next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == name) handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H") responses = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_responses") diff --git a/apps/desktop/assets/codex-bridge/test_denetim_ek.py b/apps/desktop/assets/codex-bridge/test_denetim_ek.py index 93f892a..7d7d226 100644 --- a/apps/desktop/assets/codex-bridge/test_denetim_ek.py +++ b/apps/desktop/assets/codex-bridge/test_denetim_ek.py @@ -22,7 +22,7 @@ import pencere import resim_context import yarim -SOURCE = Path(__file__).with_name("bridge.py").read_text(encoding="utf-8") +SOURCE = Path(__file__).with_name("kopru.py").read_text(encoding="utf-8") tree = ast.parse(SOURCE) handler = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "H") diff --git a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py index 590d47c..cea777a 100644 --- a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py +++ b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py @@ -7,7 +7,7 @@ from pathlib import Path import pencere -tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti", "sema_duzelt", "arac_semalarini_duzelt"} nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names] diff --git a/apps/desktop/assets/codex-bridge/test_model_switch.py b/apps/desktop/assets/codex-bridge/test_model_switch.py index a149c2e..9ef512f 100644 --- a/apps/desktop/assets/codex-bridge/test_model_switch.py +++ b/apps/desktop/assets/codex-bridge/test_model_switch.py @@ -6,7 +6,7 @@ from pathlib import Path import unittest -tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter", "phase_damgala", "asistan_mesaji_mi"} nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names] diff --git a/apps/desktop/assets/codex-bridge/test_pencere_phase.py b/apps/desktop/assets/codex-bridge/test_pencere_phase.py index bbb3109..b011dfe 100644 --- a/apps/desktop/assets/codex-bridge/test_pencere_phase.py +++ b/apps/desktop/assets/codex-bridge/test_pencere_phase.py @@ -10,7 +10,7 @@ import ide_catalog as c import pencere import resim_context -tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi"} nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names] for node in tree.body: diff --git a/apps/desktop/assets/codex-bridge/test_zstd.py b/apps/desktop/assets/codex-bridge/test_zstd.py index 7ac1723..ad5e7e1 100644 --- a/apps/desktop/assets/codex-bridge/test_zstd.py +++ b/apps/desktop/assets/codex-bridge/test_zstd.py @@ -3,7 +3,7 @@ import ast import unittest from pathlib import Path -tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")) +tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")) wanted = {"_zstd_decompress", "_zstd_ready", "decompress"} env = {} exec(compile(ast.Module(body=[n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in wanted],