"""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, 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 SURUM = "6" HERE = os.path.dirname(os.path.abspath(__file__)) # 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 PORT = int(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) DEBUG = bool(CFG.get("debug", False)) DISPLAY_MAP = CFG.get("display_map", {}) 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("%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] def load_ide_catalog(): """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) 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"]}) 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}") load_ide_catalog() def catalog_refresh_loop(): """Kataloglari arka planda yeniler; liste istegini yavaslatmaz.""" minutes = max(1, int(CFG.get("catalog_refresh_min", 5))) 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() 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 = yol + ".tmp" 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 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 class SseRewriter: """function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE.""" def __init__(self, custom): self.custom = custom self.buf = b"" self.custom_ids = set() def _fix(self, d): t = d.get("type", "") item = d.get("item") if item and item.get("type") == "function_call" and item.get("name") in self.custom: self.custom_ids.add(item.get("id")) args = item.get("arguments") or "" try: inp = json.loads(args).get("input", "") if args else "" except Exception: inp = args d["item"] = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"), "name": item.get("name"), "input": inp, "status": item.get("status", "completed")} # Normalize before Codex persists the response. Request-only repair # cannot help when the user later disables the bridge. fix_tool_ids({"input": [d["item"]]}) return d if t.startswith("response.function_call_arguments.") and d.get("item_id") in self.custom_ids: return None # JSON parcalari custom input'a cevrilemez; done olayinda tam metin gidiyor if t == "response.completed" and d.get("response", {}).get("output"): out = [] for item in d["response"]["output"]: if item.get("type") == "function_call" and item.get("name") in self.custom: try: inp = json.loads(item.get("arguments") or "{}").get("input", "") except Exception: inp = item.get("arguments", "") item = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"), "name": item.get("name"), "input": inp, "status": "completed"} fix_tool_ids({"input": [item]}) out.append(item) d["response"]["output"] = out return d def feed(self, chunk): self.buf += chunk out = b"" while b"\n\n" in self.buf: block, self.buf = self.buf.split(b"\n\n", 1) lines = block.split(b"\n") data_idx = [i for i, l in enumerate(lines) if l.startswith(b"data:")] if not data_idx: out += block + b"\n\n"; continue try: d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx)) except Exception: out += block + b"\n\n"; continue d = self._fix(d) if d is None: continue ev = [l for l in lines if l.startswith(b"event:")] evline = (b"event: " + d.get("type", "").encode() + b"\n") if ev else b"" out += evline + b"data: " + json.dumps(d, ensure_ascii=False).encode("utf-8") + b"\n\n" return out def flush(self): b, self.buf = self.buf, b"" return b RESP_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"} REQ_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"} def claude_sonu_duzelt(req): """9Router Claude cevirisi developer/system mesajlarini dusurur; geriye kalan son oge asistan mesaji ya da reasoning ise Claude 'assistant message prefill' hatasi (400) verir. O durumda sona kisa user mesaji eklenir. Doner: (eklendi_mi, son ogelerin kisa ozeti).""" items = req.get("input") if not isinstance(items, list): return False, "" ozet = [] for it in reversed(items): if not isinstance(it, dict): continue tip, rol = it.get("type", "message"), it.get("role") ozet.append(f"{tip}/{rol}" if rol else tip) if tip == "message" and rol in ("developer", "system"): continue if (tip == "message" and rol == "assistant") or tip == "reasoning": items.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": "(Önceki cevabın yarıda kaldı ya da sesli konuşma kapandı; bağlama göre devam et.)"}]}) return True, ",".join(ozet[:3]) return False, "" return False, "" def decompress(body, enc): enc = (enc or "").lower().strip() if enc == "gzip": import gzip; return gzip.decompress(body) if enc == "deflate": import zlib; return zlib.decompress(body) if enc == "zstd": return _zstd_decompress(body) if enc == "br": import brotli; return brotli.decompress(body) return body OZET_FRENI = ozet_kopru.DonguFreni() 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: line = self.rfile.readline().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): 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 def w(b): if b: self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush() try: while True: chunk = resp.read(4096) if not chunk: break total += len(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)") log(f"{tag} bitti status={status} {total} bayt") 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, "zstd": ZSTD_OK, "ide_durum": durum, "ide_model": int((sayi.split() or ["0"])[0]), "ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False))}) 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.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): 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}"); 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 "-" tok = (self.headers.get("Authorization") or "")[-8:] if status == 200: out = self._models(resp, tag) return out body = resp.read()[:300] log(f"{tag} chatgpt.com {status} verdi (hesap={acct} token=..{tok}) govde={body[:160]!r}") cached = LAST_MODELS.get(acct) if cached: 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 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) 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 for m in models: new = DISPLAY_MAP.get(m.get("slug")) if new: m["display_name"] = new; changed += 1 by_slug = {m.get("slug"): m for m in models} for x in EXTRA_MODELS: 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 m["additional_speed_tiers"] = []; m["service_tiers"] = [] 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 EXTRA_MODELS: ide_catalog.sirala_codex(models, EXTRA_MODELS) 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) 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 enc_out: hdrs_out.append(("Content-Encoding", enc_out)) if changed and not enc_out: acct_ = self.headers.get("chatgpt-account-id") or "-" onceki = LAST_MODELS.get(acct_) LAST_MODELS[acct_] = {"headers": hdrs_out, "body": out, "at": time.strftime("%H:%M:%S")} # Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz. if not onceki or onceki.get("body") != out: save_last_models(acct_, hdrs_out, 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 _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", "") mapping = MODEL_MAP.get(slug) 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 catalog.get("enabled") and slug.startswith(prefix): # 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) 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") 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: # abonelik modeli: chatgpt.com'a oldugu gibi fixed = fix_tool_ids(req) atilan = drop_foreign_items(req) degisti = False ozet_akisi = None if ozet_acik: # Ozet bagdastirici (ozet_kopru.py; 9 Ekim 2026'da yalniz bu parca geri acildi): koprunun urettigi # duz metin ozetleri OpenAI modeline de mesaj olarak gider. try: ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=False) # kapsam "hepsi": OpenAI modelinde de ozeti kopru DUZ METIN urettirir; boylece ozet sonradan # secilen her saglayicida okunur. kapsam "9router": OpenAI ozeti yerlesik (sifreli) kalir. if ozet_cfg.get("kapsam", "hepsi") == "hepsi" and ozet_kopru.tetik_mi(req): OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)) if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()): log(f"[{n} CHATGPT {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi") 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")) ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem")) ozet_akisi = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg)) ob["acilan"] += 1 log(f"[{n} CHATGPT {slug}] ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)") except Exception as e: ob = {"acilan": 0}; ozet_akisi = None log(f"[{n} CHATGPT {slug}] ozet kopru atlandi: {type(e).__name__}: {e}") if ob["acilan"]: degisti = True if ozet_akisi is None: log(f"[{n} CHATGPT {slug}] ozet kopru: {ob['acilan']} kopru ozeti duz metne acildi") if fixed or atilan or degisti: body = json.dumps(req, ensure_ascii=False).encode("utf-8") parcalar = [] if fixed: parcalar.append(f"{fixed} arac kimligi duzeltildi") if atilan: parcalar.append(f"{atilan} yabanci oge atildi") if parcalar: log(f"[{n} CHATGPT {slug}] " + ", ".join(parcalar)) if ozet_akisi is not None: sonuc = self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=True, rewriter=ozet_akisi) log(f"[{n} CHATGPT {slug}] ozet kopru: {ozet_akisi.sonuc_notu()}") return sonuc return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=bool(fixed or atilan or degisti)) 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", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)) 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[route_name] tag = f"[{n} {route_name.upper()} {slug}->{target}]" req["model"] = target req.pop("client_metadata", None) 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)") rewriter = None if flat if flat is not None else route.get("flatten_tools"): custom = flatten_request(req) rewriter = SseRewriter(custom) fix_tool_ids(req) 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). cikan, kazanc = resimleri_sadelestir(req) if cikan: log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)") 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") 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()[:400] last = (e.code, errbody) 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}") self.send_response(e.code) self.send_header("Content-Type", "application/json") 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 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) 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()