Measured with the unmodified Codex binary on 10 Oct 2026: - real context windows per model (NanoGPT list, local table, combo = its smallest member, capped at the summarizer's window) so Codex compacts by itself when switching to a smaller model; an overflow guard trims oversized old parts and reports what still does not fit - classic tools instead of code mode for external models: GLM 0/3 -> 3/3, Qwen 0/3 -> 2/3 on simple tool tasks, Claude/cx/Gemini unchanged - phase (commentary / final_answer) on external assistant messages - an answer cut by the client is carried to the next turn once - images stay for vision-capable targets - summary brake stops tight loops, not frequent compaction - permanent gateway errors and tool-less targets fail at once with a readable message instead of five reconnects - a lone "." (used to apply a model change in voice) is answered locally - the bridge listens within seconds; the IDE waits 45 s for it Release: AITURK IDE 1.0.0-beta.26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1235 lines
66 KiB
Python
1235 lines
66 KiB
Python
"""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/<id>" 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 <dir> 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
|
||
import pencere
|
||
import yarim
|
||
|
||
SURUM = "7"
|
||
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 <value> 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]
|
||
IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz)
|
||
|
||
def load_ide_catalog(ag=True):
|
||
"""IDE'nin gecit katalogunu "ide/<id>" 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 ""}
|
||
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()
|
||
|
||
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
|
||
|
||
|
||
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):
|
||
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 _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):
|
||
# 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 [{"type": "response.failed", "response": {
|
||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||
"error": {"code": "invalid_request_error", "message":
|
||
"Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka 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"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
|
||
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key"
|
||
d = {"type": "response.failed", "response": {
|
||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}}
|
||
t = "response.failed"
|
||
item = d.get("item") if isinstance(d.get("item"), dict) else None
|
||
if item and item.get("type") in ARAC_OGELERI:
|
||
self.arac_goruldu = True
|
||
once = []
|
||
if self.bekleyen is not None and (t == "response.output_item.added"
|
||
or (t == "response.output_item.done" and item is not None)):
|
||
once.append(self._birak("commentary"))
|
||
if t in ("response.completed", "response.incomplete", "response.failed") and self.bekleyen is not None:
|
||
once.append(self._birak("commentary" if self.arac_goruldu else "final_answer"))
|
||
if t == "response.completed" and isinstance((d.get("response") or {}).get("output"), list):
|
||
phase_damgala(d["response"]["output"])
|
||
if t == "response.failed" and self.tasma is not None:
|
||
hata = (d.get("response") or {}).get("error") or {}
|
||
metin = str(hata.get("message") or "")
|
||
if pencere.tasma_mi(metin) or pencere.tasma_mi(str(hata.get("code") or "")):
|
||
hata["code"] = "context_length_exceeded" # Codex'in tanidigi tasma kodu
|
||
self.tasma(self.hedef, metin)
|
||
if t == "response.output_item.done" and asistan_mesaji_mi(item) and not item.get("phase"):
|
||
if self.arac_goruldu:
|
||
item["phase"] = "commentary"
|
||
else:
|
||
self.bekleyen = d
|
||
return once
|
||
return once + [d]
|
||
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:")]
|
||
for o in self._sirala(d):
|
||
evline = (b"event: " + o.get("type", "").encode() + b"\n") if ev else b""
|
||
out += evline + b"data: " + json.dumps(o, ensure_ascii=False).encode("utf-8") + b"\n\n"
|
||
return out
|
||
def flush(self):
|
||
b, self.buf = self.buf, b""
|
||
if self.bekleyen is not None:
|
||
# Akis bitis olayi gelmeden koptu: bekleyen mesaj yutulmaz, isaretsiz haliyle iletilir.
|
||
o, self.bekleyen = self.bekleyen, None
|
||
b = (b"event: " + o.get("type", "").encode() + b"\ndata: "
|
||
+ json.dumps(o, ensure_ascii=False).encode("utf-8") + b"\n\n") + 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"}
|
||
|
||
NOKTA_ISARETLERI = {".", "。"}
|
||
|
||
def nokta_mesaji_mi(req):
|
||
"""Son girdi, metni yalnizca "." olan bir kullanici mesaji mi? Kullanici sesli konusmada model degistirdikten
|
||
sonra secimin uygulanmasi icin tek bir "." yaziyor (10 Ekim 2026). Bu mesaj bir istek degildir."""
|
||
girdi = req.get("input")
|
||
if not isinstance(girdi, list) or not girdi:
|
||
return False
|
||
son = girdi[-1]
|
||
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
|
||
return False
|
||
icerik = son.get("content")
|
||
if isinstance(icerik, str):
|
||
metin = icerik
|
||
elif isinstance(icerik, list) and all(isinstance(p, dict) and p.get("type") in ("input_text", "text") for p in icerik):
|
||
metin = "".join(str(p.get("text") or "") for p in icerik)
|
||
else:
|
||
return False # resim, dosya vb. tasiyan mesaj nokta mesaji sayilmaz
|
||
return metin.strip() in NOKTA_ISARETLERI
|
||
|
||
def hazir_yaniti(slug, metin):
|
||
"""Modele gitmeden uretilen, Codex'in tam bir yanit olarak kabul ettigi kisa akis."""
|
||
damga = "%d" % int(time.time() * 1000)
|
||
rid, mid = "resp_astrabridge_" + damga, "msg_astrabridge_" + damga
|
||
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
|
||
"content": [{"type": "output_text", "text": metin, "annotations": []}]}
|
||
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
|
||
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
|
||
olaylar = [
|
||
{"type": "response.created", "response": {"id": rid, "object": "response", "status": "in_progress",
|
||
"model": slug, "output": []}},
|
||
{"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": metin},
|
||
{"type": "response.output_item.done", "output_index": 0, "item": oge},
|
||
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
|
||
"model": slug, "output": [oge], "usage": kullanim}},
|
||
]
|
||
return b"".join(b"event: " + o["type"].encode() + b"\ndata: " + json.dumps(o, ensure_ascii=False).encode("utf-8")
|
||
+ b"\n\n" for o in olaylar)
|
||
|
||
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, 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:
|
||
chunk = resp.read(4096)
|
||
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, "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, 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}"); 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, izle)
|
||
|
||
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
|
||
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 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)
|
||
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.
|
||
ad = next((m.get("display_name") for m in EXTRA_MODELS if m.get("slug") == slug), None) or slug
|
||
govde = hazir_yaniti(slug, f"{ad} hazır.")
|
||
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
|
||
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)
|
||
return
|
||
# Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir.
|
||
sohbet = str(req.get("prompt_cache_key") or "")
|
||
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 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 = bool(yarim_eklenen)
|
||
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", 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} 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), izle=izle)
|
||
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[route_name]
|
||
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)")
|
||
rewriter = None
|
||
if flat if flat is not None else route.get("flatten_tools"):
|
||
custom = flatten_request(req)
|
||
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
|
||
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, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
|
||
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")
|
||
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
|
||
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
|
||
pencere_biliniyor = "varsayilan" not in ((IDE_BILGI.get(slug) or {}).get("kaynak") or "varsayilan")
|
||
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
|
||
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere)
|
||
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()[: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}")
|
||
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")
|
||
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, 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()
|