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],