1704 lines
95 KiB
Python
1704 lines
95 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, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
|
||
from gemini_context import coalesce_context
|
||
try:
|
||
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
|
||
except ImportError:
|
||
def pano_blogunu_ulastir(req):
|
||
return 0, ""
|
||
from resim_context import resimleri_sadelestir
|
||
import ide_catalog
|
||
import ozet_kopru
|
||
import pencere
|
||
import yarim
|
||
import model_gecis
|
||
import is_hatti
|
||
import internet
|
||
import jev_denetim
|
||
|
||
SURUM = "8"
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
try:
|
||
with open(os.path.abspath(__file__), "rb") as _f:
|
||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] # ayni surum numarasi altindaki farkli kodu ayirt eder
|
||
except OSError:
|
||
KOD = ""
|
||
|
||
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
|
||
# interpreters the matching `zstandard` build ships next to this file.
|
||
_VENDOR = os.path.join(HERE, "vendor", "cp%d%d-win_amd64" % sys.version_info[:2])
|
||
if os.name == "nt" and 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
|
||
# Ayiricinin (bridge.py) arkasinda calisirken kapi --port ile verilir; tek basina calisirken bridge.json'daki kapidir.
|
||
PORT = int(_arg("--port", "AITURK_CODEX_KOPRU_PORT") or CFG.get("port", 8790))
|
||
CHATGPT = CFG.get("chatgpt_backend", "https://chatgpt.com").rstrip("/")
|
||
ROUTES = dict(CFG.get("routes") or {}) # {"cheapvibecode": {"url","key","flatten_tools"}, ...}
|
||
RETRIES = int(CFG.get("retries", 6))
|
||
# Socket idle (per read) timeout toward 9Router/chatgpt.com. 9Router and OmniRoute keep
|
||
# long answers alive with a byte every 15 s, so this only fires on a dead peer; 60+ min
|
||
# per user decision (28 Sep 2026) so a heavy Fusion is never cut here.
|
||
UPSTREAM_IDLE_TIMEOUT = float(CFG.get("upstream_idle_timeout", 3600))
|
||
|
||
def is_timeout(error):
|
||
"""True for a socket/read timeout, also when urllib wraps it in URLError."""
|
||
reason = getattr(error, "reason", None)
|
||
return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError)
|
||
def kopma_mi(error):
|
||
"""Baglanti kurulduktan sonra uzak uc kapatti ya da sifirladi (yanit basliklari gelmeden): istek ulasmis ve
|
||
is baslamis olabilir. Baglanti hic kurulamadiysa (reddedildi, DNS) False."""
|
||
reason = getattr(error, "reason", None)
|
||
turler = (ConnectionResetError, ConnectionAbortedError, http.client.IncompleteRead) # RemoteDisconnected dahil
|
||
return isinstance(error, turler) or isinstance(reason, turler)
|
||
DEBUG = bool(CFG.get("debug", False))
|
||
UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
|
||
# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir.
|
||
NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"])
|
||
LOGDIR = os.path.join(HOME, "log")
|
||
os.makedirs(LOGDIR, exist_ok=True)
|
||
lock = threading.Lock()
|
||
seq = [0]
|
||
|
||
def log(msg):
|
||
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
|
||
print(line, flush=True)
|
||
with lock:
|
||
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
|
||
f.write(line + "\n")
|
||
|
||
# ---- model yonlendirme tablosu: slug -> (route, hedef model) ----
|
||
MODEL_MAP = {} # slug -> (route, hedef model, flatten_tools or None)
|
||
for slug, v in CFG.get("model_map", {}).items():
|
||
if isinstance(v, str):
|
||
v = {"route": "cheapvibecode", "model": v}
|
||
MODEL_MAP[slug] = (v["route"], v["model"], v.get("flatten_tools"))
|
||
|
||
EXTRA_MODELS = list(CFG.get("extra_models", []))
|
||
for m in EXTRA_MODELS:
|
||
MODEL_MAP.setdefault(m["slug"], (m.get("route", "cheapvibecode"), m.get("target", m["slug"]), m.get("flatten_tools")))
|
||
|
||
def load_9router_catalog():
|
||
global MODEL_MAP, EXTRA_MODELS
|
||
cat = CFG.get("nine_router_catalog") or {}
|
||
if not cat.get("enabled"):
|
||
return
|
||
route = cat.get("route", "9router")
|
||
r = ROUTES.get(route)
|
||
if not r:
|
||
return
|
||
prefix = cat.get("prefix", "9r/")
|
||
try:
|
||
req = urllib.request.Request(r["url"].rsplit("/responses", 1)[0] + "/models",
|
||
headers={"Authorization": f"Bearer {r['key']}", "User-Agent": UA})
|
||
data = json.load(urllib.request.urlopen(req, timeout=30))
|
||
except Exception as e:
|
||
log(f"9Router katalogu alinamadi: {e}")
|
||
return
|
||
model_map = {k: v for k, v in MODEL_MAP.items() if not k.startswith(prefix)}
|
||
extra_models = [m for m in EXTRA_MODELS if not m["slug"].startswith(prefix)]
|
||
n = 0
|
||
# Secicide yalniz "saglayici/model" gorunur (kullanici istegi, 9 Ekim 2026).
|
||
adlar = ide_catalog.adlandir([(m.get("id", ""), m.get("id", "")) for m in data.get("data", []) if m.get("id")])
|
||
for m in data.get("data", []):
|
||
mid = m.get("id", "")
|
||
slug = prefix + mid
|
||
if not mid or slug in model_map:
|
||
continue
|
||
extra_models.append({"slug": slug, "display_name": adlar.get(mid, mid),
|
||
"description": f"9Router: {mid}", "route": route, "target": mid,
|
||
"clone_from": cat.get("clone_from", "gpt-6-astra"),
|
||
"priority": 100 + n, "reasoning_levels": cat.get("reasoning_levels"),
|
||
"context_window": m.get("context_window") or m.get("context_length")})
|
||
model_map[slug] = (route, mid, None)
|
||
n += 1
|
||
MODEL_MAP = model_map
|
||
EXTRA_MODELS = extra_models
|
||
log(f"9Router katalogu: {n} model eklendi")
|
||
|
||
load_9router_catalog()
|
||
|
||
# ---- AITURK IDE "Codex Baglama": IDE'nin model/combolari (ide_catalog.py, 9 Ekim 2026) ----
|
||
IDE_PREFIX = "ide/"
|
||
IDE_ROUTE = "aiturk-ide"
|
||
IDE_DURUM = [None]
|
||
IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz)
|
||
|
||
def load_ide_catalog(ag=True):
|
||
"""IDE'nin gecit katalogunu "ide/<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 "", "claude": bool(g.get("claude"))}
|
||
model_map[slug] = (IDE_ROUTE, g["id"], None)
|
||
n += 1
|
||
else:
|
||
ROUTES.pop(IDE_ROUTE, None)
|
||
MODEL_MAP = model_map
|
||
EXTRA_MODELS = extra_models
|
||
ozet = f"{durum}, {n} model"
|
||
if IDE_DURUM[0] != ozet: # 5 dakikada bir ayni satiri yazmamak icin
|
||
IDE_DURUM[0] = ozet
|
||
log(f"AITURK IDE Codex Baglama: {ozet}")
|
||
|
||
# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler);
|
||
# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir.
|
||
load_ide_catalog(ag=False)
|
||
|
||
def catalog_refresh_loop():
|
||
"""Kataloglari arka planda yeniler; liste istegini yavaslatmaz."""
|
||
minutes = max(1, int(CFG.get("catalog_refresh_min", 5)))
|
||
time.sleep(3)
|
||
try:
|
||
load_ide_catalog() # acilistan hemen sonra: guncel pencere verisiyle
|
||
except Exception as e:
|
||
log(f"katalog yenileme hatasi: {e}")
|
||
while True:
|
||
time.sleep(minutes * 60)
|
||
for yukle in (load_9router_catalog, load_ide_catalog):
|
||
try:
|
||
yukle()
|
||
except Exception as e:
|
||
log(f"katalog yenileme hatasi: {e}")
|
||
|
||
threading.Thread(target=catalog_refresh_loop, daemon=True).start()
|
||
|
||
# ---- AITURK is hatti: isi gonderebilen AITURK komutu (is_hatti.py). Kurulum sonradan gelebilir: yoksa 2 dk'da bir bakilir.
|
||
HAT_KOMUT = [None]
|
||
|
||
def hat_komut_loop():
|
||
while True:
|
||
try:
|
||
if (CFG.get("aiturk_hat") or {}).get("enabled") is not False:
|
||
bulunan = is_hatti.komut_bul()
|
||
if bulunan != HAT_KOMUT[0]:
|
||
HAT_KOMUT[0] = bulunan
|
||
log(f"AITURK is hatti: {'acik, komut ' + bulunan if bulunan else 'kapali (aiturk is komutu yok)'}")
|
||
except Exception as e:
|
||
log(f"AITURK is hatti komutu aranamadi: {type(e).__name__}: {e}")
|
||
time.sleep(600 if HAT_KOMUT[0] else 120)
|
||
|
||
threading.Thread(target=hat_komut_loop, daemon=True).start()
|
||
|
||
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
|
||
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
|
||
|
||
def _cache_path(acct):
|
||
guvenli = re.sub(r"[^A-Za-z0-9._-]", "_", acct or "-")[:64]
|
||
return os.path.join(CACHE_DIR, f"models-{guvenli}.json")
|
||
|
||
def save_last_models(acct, hdrs, body):
|
||
"""Son basarili listeyi diske yazar. Kopru yeniden baslayinca bellek
|
||
sifirlandigi icin, restarttan sonraki ilk 401 listeyi silip goturuyordu."""
|
||
try:
|
||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||
yol = _cache_path(acct)
|
||
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||
"body": body.decode("utf-8")}
|
||
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # ayni hesabin iki es zamanli kaydi carpismasin
|
||
with open(gecici, "w", encoding="utf-8") as f:
|
||
json.dump(veri, f, ensure_ascii=False)
|
||
os.replace(gecici, yol) # yarim dosya birakmamak icin
|
||
except Exception as e:
|
||
log(f"son liste diske yazilamadi: {e}")
|
||
|
||
def load_last_models():
|
||
"""Acilista diskteki son listeleri bellege alir."""
|
||
try:
|
||
if not os.path.isdir(CACHE_DIR):
|
||
return
|
||
for ad in os.listdir(CACHE_DIR):
|
||
if not (ad.startswith("models-") and ad.endswith(".json")):
|
||
continue
|
||
try:
|
||
with open(os.path.join(CACHE_DIR, ad), encoding="utf-8") as f:
|
||
v = json.load(f)
|
||
acct = v.get("acct") or ad[len("models-"):-len(".json")]
|
||
LAST_MODELS[acct] = {"headers": [tuple(h) for h in v["headers"]],
|
||
"body": v["body"].encode("utf-8"), "at": v.get("at", "?")}
|
||
log(f"diskten son liste alindi ({acct}, {len(v['body'])} bayt, {v.get('at')})")
|
||
except Exception as e:
|
||
log(f"son liste okunamadi {ad}: {e}")
|
||
except Exception as e:
|
||
log(f"son liste klasoru okunamadi: {e}")
|
||
SLIM_CLONES = bool(CFG.get("slim_clones", False))
|
||
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
|
||
|
||
# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ----
|
||
def sema_duzelt(sema):
|
||
"""Arac parametre semasinin EN UST duzeyindeki oneOf/anyOf/allOf'u tek nesne semasina indirir.
|
||
Claude bunu reddediyor ("input_schema does not support oneOf, allOf, or anyOf at the top level"; olcum
|
||
11 Ekim 2026: masaustu Codex'in klasik arac listesindeki bir arac yuzunden her Opus istegi 400 aldi).
|
||
Secenek nesnelerinin alanlari birlestirilir; zorunlu alanlar oneOf/anyOf'ta kesisim, allOf'ta birlesimdir.
|
||
Donus: (sema, degisti mi)."""
|
||
if not isinstance(sema, dict) or not any(k in sema for k in ("oneOf", "anyOf", "allOf")):
|
||
return sema, False
|
||
yeni = {k: v for k, v in sema.items() if k not in ("oneOf", "anyOf", "allOf")}
|
||
alanlar = dict(yeni.get("properties") or {})
|
||
zorunlu = set(yeni.get("required") or [])
|
||
for anahtar in ("oneOf", "anyOf", "allOf"):
|
||
secenekler = [x for x in (sema.get(anahtar) or []) if isinstance(x, dict)]
|
||
kesisim = None
|
||
for secenek in secenekler:
|
||
secenek, _ = sema_duzelt(secenek)
|
||
for ad, tanim in (secenek.get("properties") or {}).items():
|
||
alanlar.setdefault(ad, tanim)
|
||
gerekli = set(secenek.get("required") or [])
|
||
if anahtar == "allOf":
|
||
zorunlu |= gerekli
|
||
else:
|
||
kesisim = gerekli if kesisim is None else (kesisim & gerekli)
|
||
if anahtar != "allOf" and kesisim:
|
||
zorunlu |= kesisim
|
||
yeni["type"] = "object"
|
||
yeni["properties"] = alanlar
|
||
if zorunlu:
|
||
yeni["required"] = sorted(a for a in zorunlu if a in alanlar)
|
||
else:
|
||
yeni.pop("required", None)
|
||
return yeni, True
|
||
|
||
def arac_semalarini_duzelt(req):
|
||
"""Dis modele giden duz arac listesinde ust duzey birlesimli semalari duzeltir. Donus: duzeltilen arac sayisi."""
|
||
n = 0
|
||
for t in req.get("tools") or []:
|
||
if isinstance(t, dict) and isinstance(t.get("parameters"), dict):
|
||
t["parameters"], degisti = sema_duzelt(t["parameters"])
|
||
n += bool(degisti)
|
||
return n
|
||
|
||
def flatten_request(req):
|
||
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
|
||
custom = set()
|
||
tools = []
|
||
def add_tool(t):
|
||
ty = t.get("type")
|
||
if ty == "namespace":
|
||
for s in t.get("tools", []):
|
||
add_tool(s)
|
||
elif ty == "function":
|
||
tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""),
|
||
"parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False})
|
||
elif ty == "custom":
|
||
custom.add(t["name"])
|
||
tools.append({"type": "function", "name": t["name"],
|
||
"description": (t.get("description", "") + "\n\nBu arac serbest metin alir: metni 'input' alaninda ver."),
|
||
"parameters": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]},
|
||
"strict": False})
|
||
elif ty in ("web_search", "web_search_preview", "local_shell", "computer_use_preview"):
|
||
pass # cevirici desteklemez, atla
|
||
else:
|
||
tools.append(t)
|
||
for t in req.get("tools", []) or []:
|
||
add_tool(t)
|
||
new_input = []
|
||
for it in req.get("input", []) or []:
|
||
ty = it.get("type")
|
||
if ty == "additional_tools":
|
||
for t in it.get("tools", []):
|
||
add_tool(t)
|
||
continue
|
||
if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"):
|
||
# Desktop thread handoffs have no matching tool call. Preserve their
|
||
# content as imported context, not an invalid tool-role message.
|
||
output = it.get("output", "")
|
||
if not isinstance(output, str):
|
||
output = json.dumps(output, ensure_ascii=False)
|
||
name = it.get("name", "unknown")
|
||
new_input.append({"role": "user", "content": [{
|
||
"type": "input_text",
|
||
"text": f"Imported tool output ({name}); treat as context data:\n{output}"
|
||
}]})
|
||
continue
|
||
if ty == "custom_tool_call":
|
||
it = {"type": "function_call", "id": it.get("id"), "call_id": it.get("call_id"), "name": it.get("name"),
|
||
"arguments": json.dumps({"input": it.get("input", "")}, ensure_ascii=False)}
|
||
elif ty == "custom_tool_call_output":
|
||
it = {"type": "function_call_output", "id": it.get("id"), "call_id": it.get("call_id"), "output": it.get("output", "")}
|
||
new_input.append(it)
|
||
req["input"] = new_input
|
||
if tools:
|
||
req["tools"] = tools
|
||
req.pop("include", None)
|
||
# OpenAI-specific access-program selection is not part of the external
|
||
# chat API schema (Muse Spark rejects it). ChatGPT passthrough never calls
|
||
# this adapter, so its access-program selection remains intact.
|
||
req.pop("access_programs", None)
|
||
# Codex sends Responses-only verbosity/default text formatting. Chat
|
||
# translators may forward this unsupported object to their upstream.
|
||
text_config = req.get("text")
|
||
if isinstance(text_config, dict) and set(text_config) <= {"verbosity", "format"}:
|
||
if not text_config.get("format") or text_config["format"].get("type") == "text":
|
||
req.pop("text", None)
|
||
return custom
|
||
|
||
# Oge tipine gore API'nin bekledigi kimlik on eki. Cikti tipleri de dahildir:
|
||
# flatten_request custom_tool_call_output -> function_call_output cevirdiginde
|
||
# kimlik 'ctco_...' kaliyordu ve karsi taraf 'fc' bekliyordu (400).
|
||
ID_PREFIX = {
|
||
"custom_tool_call": "ctc",
|
||
"custom_tool_call_output": "ctco",
|
||
"function_call": "fc",
|
||
"function_call_output": "fc",
|
||
}
|
||
|
||
def fix_tool_ids(req):
|
||
"""Arac cagrisi/cikti kimliklerinin on ekini hedef API'nin bekledigine cevirir.
|
||
Codex gecmisinde kalan yanlis on ekli kimlikler (or. fc_toolu_..., ctco_...)
|
||
400 hatasina yol aciyordu."""
|
||
changed = 0
|
||
for it in req.get("input", []) or []:
|
||
if not isinstance(it, dict):
|
||
continue
|
||
want = ID_PREFIX.get(it.get("type"))
|
||
i = it.get("id")
|
||
if not want or not isinstance(i, str) or not i:
|
||
continue
|
||
# Alt cizgi ile karsilastir: 'ctco_x'.startswith('ctc') dogru dondugu icin
|
||
# ctco kimlikleri yanlislikla "zaten dogru" sayiliyordu.
|
||
if i.startswith(want + "_"):
|
||
continue
|
||
tail = i.split("_", 1)[1] if "_" in i else i
|
||
it["id"] = want + "_" + tail
|
||
changed += 1
|
||
return changed
|
||
|
||
|
||
# Cevirici uretimi kimliklerin izi. 9Router chat.completions'a cevirdigi icin
|
||
# uydurdugu kimliklerde bu parca bulunur; chatgpt.com boyle bir kaydi saklamaz.
|
||
FOREIGN_ID_MARK = "chatcmpl-"
|
||
|
||
# Icerigini tasimayan, yalnizca hedefteki kayda ISARET EDEN oge tipleri.
|
||
# Kimlik yabanciysa hedef cozemez; ceviriyle kurtarilamaz, atilmalari gerekir.
|
||
REFERENCE_TYPES = ("reasoning", "item_reference")
|
||
|
||
# Icerigini kendisi tasiyan tipler: yabanci kimlik tasisalar da sorun degil,
|
||
# uyari uretmezler. ID_PREFIX'teki tipler de bu gruba dahildir.
|
||
INLINE_TYPES = ("message",)
|
||
|
||
def drop_foreign_items(req):
|
||
"""Baska bir saglayicinin urettigi, hedefin cozemeyecegi ogeleri atar.
|
||
|
||
Ilke hermes-agent'tan: sahipli kimligi tasima. hermes gecmisi saglayicidan
|
||
bagimsiz tuttugu icin model degistirince hicbir sey kirilmiyor; Codex ise
|
||
Responses bicimindeki sahipli kimlikleri tasiyor. Model degistirildiginde
|
||
chatgpt.com 404 'Item ... not found. Items are not persisted when store is
|
||
set to false' diyor ve tur hic baslamiyordu.
|
||
|
||
Icerigini iceren ogeler (function_call vb.) atilmaz; onlarin kimligi
|
||
fix_tool_ids ile duzeltilir. Yalnizca isaret eden tipler atilir."""
|
||
inp = req.get("input")
|
||
if not isinstance(inp, list):
|
||
return 0
|
||
kalan, atilan, bilinmeyen = [], 0, set()
|
||
for it in inp:
|
||
i = it.get("id") if isinstance(it, dict) else None
|
||
if isinstance(i, str) and FOREIGN_ID_MARK in i:
|
||
ty = it.get("type")
|
||
if ty in REFERENCE_TYPES:
|
||
atilan += 1
|
||
continue
|
||
if ty not in ID_PREFIX and ty not in INLINE_TYPES:
|
||
# Tanimadigimiz bir tip yabanci kimlik tasiyor. Simdilik
|
||
# gecirilir; hata verirse gunlukte hangi tip oldugu yazili olur.
|
||
bilinmeyen.add(ty)
|
||
kalan.append(it)
|
||
if atilan:
|
||
req["input"] = kalan
|
||
if bilinmeyen:
|
||
log(f"UYARI: yabanci kimlikli taninmayan oge tipleri gecirildi: {sorted(bilinmeyen)}")
|
||
return atilan
|
||
|
||
|
||
ARAC_OGELERI = {"function_call", "custom_tool_call", "local_shell_call", "tool_search_call", "web_search_call",
|
||
"mcp_call", "computer_call", "image_generation_call", "code_interpreter_call"}
|
||
|
||
def asistan_mesaji_mi(item):
|
||
return isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "assistant"
|
||
|
||
def phase_damgala(cikti):
|
||
"""Bitmis yanitin cikti listesinde phase'i olmayan asistan mesajlarini damgalar. Kural: yanitta arac
|
||
cagrisi varsa tur suruyor demektir, butun mesajlar ara aciklamadir; yoksa yalniz SON mesaj son cevaptir.
|
||
Codex masaustu ara mesaji bu isaretle tanir; isaretsiz her mesaji son cevap sayar (10 Ekim 2026)."""
|
||
arac_var = any(isinstance(o, dict) and o.get("type") in ARAC_OGELERI for o in cikti)
|
||
mesajlar = [o for o in cikti if asistan_mesaji_mi(o)]
|
||
for o in mesajlar:
|
||
if not o.get("phase"):
|
||
o["phase"] = "commentary" if (arac_var or o is not mesajlar[-1]) else "final_answer"
|
||
|
||
class SseRewriter:
|
||
"""function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE.
|
||
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
|
||
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
|
||
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
|
||
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False):
|
||
self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor
|
||
self.custom = custom
|
||
self.buf = b""
|
||
self.custom_ids = set()
|
||
self.bekleyen = None # phase karari bekleyen "output_item.done" olayi
|
||
self.arac_goruldu = False
|
||
self.hedef = hedef # gecit model kimligi (tasma siniri ogrenmek icin)
|
||
self.tasma = tasma # callable(hedef, hata metni): sinir ogrenildiyse cagrilir
|
||
self.aracsiz = aracsiz # callable(hedef): hedef "arac cagrisi desteklenmiyor" dedi
|
||
def _birak(self, phase):
|
||
d, self.bekleyen = self.bekleyen, None
|
||
if d is not None and not d["item"].get("phase"):
|
||
d["item"]["phase"] = phase
|
||
return d
|
||
def _gorunur_hata(self, mesaj):
|
||
"""Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026)
|
||
hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda
|
||
kullanici hatayi duyar."""
|
||
damga = "%d" % int(time.time() * 1000)
|
||
rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga
|
||
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
|
||
"content": [{"type": "output_text", "text": mesaj, "annotations": []}]}
|
||
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
|
||
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
|
||
return [
|
||
{"type": "response.output_item.added", "output_index": 0,
|
||
"item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}},
|
||
{"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj},
|
||
{"type": "response.output_item.done", "output_index": 0, "item": oge},
|
||
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
|
||
"output": [oge], "usage": kullanim}},
|
||
]
|
||
|
||
def _sirala(self, d):
|
||
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
|
||
t = d.get("type", "")
|
||
if t == "error" and not isinstance(d.get("error"), dict) and d.get("message"):
|
||
# Gecidin duz bicimi: {"type":"error","code":"bad_request","message":"[400]: ...","status_code":400}
|
||
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
|
||
"status_code": d.get("status_code")}}
|
||
if t == "error" and isinstance(d.get("error"), dict):
|
||
cevrilen = self._hata_cevir(d)
|
||
if cevrilen is not None:
|
||
# Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir.
|
||
once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else []
|
||
return once + cevrilen
|
||
return self._sirala_normal(d, t)
|
||
|
||
def _hata_cevir(self, d):
|
||
"""Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir."""
|
||
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
|
||
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
|
||
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
|
||
metin = str(d["error"].get("message") or d["error"].get("type") or "")
|
||
if pencere.aracsiz_mi(metin):
|
||
if self.aracsiz is not None:
|
||
self.aracsiz(self.hedef)
|
||
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
|
||
if self.dolu and pencere.kapali_hata_mi(metin):
|
||
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
|
||
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
|
||
return [{"type": "response.failed", "response": {
|
||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||
"error": {"code": "context_length_exceeded", "message":
|
||
"Geçit isteği reddetti ve nedenini bildirmedi. Sohbet seçili modelin bağlam penceresine çok yakın; "
|
||
"sığmamış olabilir. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}]
|
||
if pencere.tasma_mi(metin):
|
||
if self.tasma is not None:
|
||
self.tasma(self.hedef, metin)
|
||
return [{"type": "response.failed", "response": {
|
||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
|
||
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
|
||
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
|
||
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
|
||
f"Ayrıntı: {metin[:200]}")
|
||
if pencere.kapali_hata_mi(metin):
|
||
return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
|
||
"yeniden deneyin.")
|
||
return None
|
||
|
||
def _sirala_normal(self, d, t):
|
||
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
|
||
if b"\r" in self.buf:
|
||
self.buf = self.buf.replace(b"\r\n", b"\n") # CRLF ile ayrilan SSE de cevrilir
|
||
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
|
||
if not isinstance(d, dict):
|
||
out += block + b"\n\n"; continue # "data: null" gibi govde akisi kesmez, oldugu gibi gecer
|
||
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 = None
|
||
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
|
||
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
|
||
continue
|
||
son = oge
|
||
break
|
||
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()
|
||
YONLENDIRME = model_gecis.Yonlendirme(HOME)
|
||
GORULEN_ISTEMCI = set() # originator degerleri; her biri gunluge bir kez yazilir
|
||
_BILINEN = {"kaynak": None, "liste": []}
|
||
|
||
def bilinen_modeller():
|
||
"""Codex'e en son verilen listedeki (slug, gorunen ad) ciftleri: abonelik modelleri + eklenenler."""
|
||
son = max(LAST_MODELS.values(), key=lambda v: str(v.get("at") or ""), default=None) if LAST_MODELS else None
|
||
govde = son.get("body") if son else None
|
||
if govde is not None and _BILINEN["kaynak"] is not govde:
|
||
try:
|
||
liste = [(m["slug"], m.get("display_name") or m["slug"]) for m in json.loads(govde).get("models", [])
|
||
if isinstance(m, dict) and m.get("slug")]
|
||
except (ValueError, AttributeError, TypeError):
|
||
liste = []
|
||
_BILINEN["kaynak"], _BILINEN["liste"] = govde, liste
|
||
liste = list(_BILINEN["liste"])
|
||
gorulen = {slug for slug, _ in liste}
|
||
liste += [(m["slug"], m.get("display_name") or m["slug"]) for m in EXTRA_MODELS
|
||
if m.get("slug") and m["slug"] not in gorulen]
|
||
return liste
|
||
|
||
def ek_model_mu(slug):
|
||
"""Bizim sonradan ekledigimiz model mi (ide/..., 9r/..., bridge.json'da tanimli)? Degilse Codex'in kendi
|
||
(abonelik) modelidir ve kopru ona DOKUNMAZ. Kullanici karari (11 Ekim 2026): "Codex'in modelleri bir tarafta.
|
||
Bizim sonradan eklediklerimiz diger tarafta. Bizim sonradan eklediklerimiz kopru kullaniyor. Codex'in
|
||
modelleri kopru kullanmiyor." """
|
||
slug = str(slug or "")
|
||
onek = (CFG.get("nine_router_catalog") or {}).get("prefix", "9r/")
|
||
return bool(slug) and (MODEL_MAP.get(slug) is not None or slug.startswith("ide/") or bool(onek and slug.startswith(onek)))
|
||
|
||
|
||
def yonlendirilen(sohbet, gelen):
|
||
"""Sohbetin sesle secilmis ek modeli; yoksa None. Codex'in kendi modeline giden eski kayit yok sayilir:
|
||
kendi modellerine sesle gecilmez, ekrandaki seciciden secilir."""
|
||
hedef = YONLENDIRME.al(sohbet, gelen)
|
||
return hedef if hedef and ek_model_mu(hedef) else None
|
||
|
||
|
||
_GECIT = {"zaman": 0.0, "liste": []}
|
||
|
||
def gecit_modelleri():
|
||
"""AITURK gecidindeki BUTUN modeller (Codex listesinde gorunmeyenler dahil), "ide/<kimlik>" olarak. Soylenen ad
|
||
Codex listesinde yoksa buraya bakilir (olcum 11 Ekim 2026: "Seed 2.1 Turbo" gecitte vardi, listede yoktu).
|
||
10 dakika bellekte tutulur; alinamazsa bos liste."""
|
||
simdi = time.time()
|
||
if simdi - _GECIT["zaman"] < 600:
|
||
return _GECIT["liste"]
|
||
_GECIT["zaman"] = simdi
|
||
rota = ROUTES.get("aiturk-ide")
|
||
if not rota:
|
||
_GECIT["liste"] = []
|
||
return []
|
||
try:
|
||
adres = rota["url"].rsplit("/", 1)[0] + "/models"
|
||
istek = urllib.request.Request(adres, headers={"Authorization": f"Bearer {rota['key']}", "User-Agent": UA})
|
||
with urllib.request.urlopen(istek, timeout=6) as yanit:
|
||
veri = json.loads(yanit.read())
|
||
_GECIT["liste"] = [("ide/" + m["id"], m["id"]) for m in veri.get("data", []) if isinstance(m, dict) and m.get("id")]
|
||
except Exception as e:
|
||
log(f"gecit model listesi alinamadi: {type(e).__name__}: {e}")
|
||
_GECIT["liste"] = []
|
||
return _GECIT["liste"]
|
||
|
||
def model_adi(slug):
|
||
return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug
|
||
|
||
class H(http.server.BaseHTTPRequestHandler):
|
||
protocol_version = "HTTP/1.1"
|
||
def log_message(self, *a): pass
|
||
|
||
def _read(self):
|
||
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
|
||
out = bytearray()
|
||
while True:
|
||
raw = self.rfile.readline()
|
||
if not raw:
|
||
break # baglanti kapandi (EOF): sonsuz donguye girme
|
||
line = raw.strip()
|
||
if not line:
|
||
continue
|
||
size = int(line.split(b";")[0], 16)
|
||
if size == 0:
|
||
while self.rfile.readline().strip():
|
||
pass
|
||
break
|
||
out += self.rfile.read(size)
|
||
self.rfile.readline()
|
||
return bytes(out)
|
||
n = int(self.headers.get("Content-Length") or 0)
|
||
return self.rfile.read(n) if n else b""
|
||
|
||
def _akis_basligi(self, resp, status):
|
||
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()
|
||
|
||
def _denetimli_akis(self, resp, tag, rewriter, izle, istek, sira, ac):
|
||
"""Katman denetimi (jev_denetim.py): cevap istemciye verilmeden once tamami okunur ve Jev'e sorulur.
|
||
Gecerse oldugu gibi verilir; gecmezse atilir ve ayni adimi siradaki katman yapar (ac(ad): (resp, rewriter,
|
||
tag)). Son katmanin cevabi sorulmadan akar. Beklerken SSE yorum satiri gider ki Codex akisi kopmus saymasin."""
|
||
if resp is None: # secili model hata verdi: cevabi yok, adimi siradaki katman yapar
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "text/event-stream")
|
||
self.send_header("Transfer-Encoding", "chunked")
|
||
self.end_headers()
|
||
else:
|
||
self._akis_basligi(resp, resp.status)
|
||
kalan, son_yazi, son_govde = list(sira), time.time(), b""
|
||
|
||
def w(b):
|
||
if b:
|
||
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
|
||
try:
|
||
while True:
|
||
parcalar, okuyan, hata = [], yarim.Izleyici("", ""), resp is None
|
||
while not hata:
|
||
try:
|
||
chunk = resp.read1(4096)
|
||
except (OSError, http.client.HTTPException) as e:
|
||
log(f"{tag} katman denetimi: cevap yarida koptu, siradakine geciliyor ({type(e).__name__}: {e})")
|
||
hata = True
|
||
break
|
||
if not chunk:
|
||
break
|
||
parcalar.append(chunk)
|
||
okuyan.feed(chunk)
|
||
if jev_denetim.hazirlik_mi(b''.join(parcalar)):
|
||
# Kullanıcı kararı 11 Ekim: araç/hazırlık adımı ret denetimine girmez.
|
||
# İlk araç olayı görüldüğünde tamponu bırak; araç çağrısının bitmesini bekleme.
|
||
log(f"{tag} Jev atlandi: arac veya hazirlik adimi, secili model korunuyor")
|
||
for parca in parcalar:
|
||
if izle is not None:
|
||
izle.feed(parca)
|
||
w(rewriter.feed(parca) if rewriter else parca)
|
||
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
|
||
if time.time() - son_yazi >= 10:
|
||
w(b": denetim\n\n"); son_yazi = time.time()
|
||
yazi = okuyan.metin.strip()
|
||
# Araç/hazırlık yukarıda aktarıldı; burada yalnız nihai cevap denetlenir.
|
||
metin = yazi
|
||
gecmedi = False
|
||
if hata:
|
||
gecmedi = True
|
||
elif metin:
|
||
t0 = time.time()
|
||
puan = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
|
||
if puan is None:
|
||
log(f"{tag} katman denetimi: Jev'e sorulamadi, cevap oldugu gibi geciyor")
|
||
else:
|
||
gecmedi = jev_denetim.karar(puan)
|
||
log(f"{tag} katman denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f} -> "
|
||
f"{'GECMEDI' if gecmedi else 'gecti'} ({time.time() - t0:.1f} sn, {len(metin)} karakter{'' if yazi else ', yalniz komut'})")
|
||
else:
|
||
# Bos adim: ne yazi var ne arac cagrisi (hata olayi da boyledir, or. "upstream 400"). Kabul
|
||
# edilmez, siradaki katman yapar; bos yanit cevap diye verilirse Codex adimi bastan dener.
|
||
gecmedi = True
|
||
log(f"{tag} katman denetimi: bos adim (yazi da komut da yok), siradakine geciliyor: "
|
||
f"{b''.join(parcalar)[-200:]!r}")
|
||
yeni = None
|
||
while gecmedi and kalan and yeni is None:
|
||
ad = kalan.pop(0)
|
||
try:
|
||
yeni = ac(ad)
|
||
except Exception as e:
|
||
son_govde = e.read() if hasattr(e, "read") else b""
|
||
log(f"{tag} katman denetimi: {ad} acilamadi ({type(e).__name__}: {e} {son_govde[:200]!r})")
|
||
if yeni is None:
|
||
break
|
||
resp, rewriter, tag = yeni
|
||
log(f"{tag} katman denetimi: adim bu katmana verildi")
|
||
if not kalan:
|
||
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
|
||
if gecmedi:
|
||
# Son katman ne dondurduyse o gider, hicbir sey eklenmez; dondurmediyse hicbir sey gitmez.
|
||
w(son_govde)
|
||
else:
|
||
for chunk in parcalar:
|
||
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 (denetim tamponu)")
|
||
return
|
||
log(f"{tag} bitti {'hata' if gecmedi else 'status=200'} {sum(map(len, parcalar))} bayt")
|
||
|
||
def _stream(self, resp, status, tag, rewriter=None, izle=None, basliksiz=False):
|
||
if not basliksiz:
|
||
self._akis_basligi(resp, status)
|
||
total = 0
|
||
bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir
|
||
def w(b):
|
||
if b:
|
||
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
|
||
try:
|
||
while True:
|
||
try:
|
||
# read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu.
|
||
chunk = resp.read1(4096)
|
||
except (OSError, http.client.HTTPException) as e:
|
||
# Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex
|
||
# yarim akisi tam sanmasin ve yeniden denesin.
|
||
log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}")
|
||
self.close_connection = True
|
||
return
|
||
if not chunk: break
|
||
total += len(chunk)
|
||
if len(bas) < 600:
|
||
bas += chunk[:600 - len(bas)]
|
||
if izle is not None:
|
||
izle.feed(chunk)
|
||
w(rewriter.feed(chunk) if rewriter else chunk)
|
||
if rewriter: w(rewriter.flush())
|
||
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
|
||
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
|
||
log(f"{tag} istemci baglantiyi kapatti ({total} bayt)")
|
||
# Yanit bitmeden istemci kapatti (durdurma ya da uygulama kapanmasi): yarim metin sonraki tura tasinir.
|
||
if izle is not None and yarim.kaydet(izle):
|
||
log(f"{tag} yarim cevap saklandi ({len(izle.metin)} karakter); ayni sohbetin sonraki istegine not olarak eklenecek")
|
||
log(f"{tag} bitti status={status} {total} bayt")
|
||
if rewriter is not None and total < 600:
|
||
log(f"{tag} kisa akis govdesi: {bas[:500]!r}")
|
||
|
||
def _tasma_ogren(self, hedef, metin):
|
||
sinir = pencere.sinir_oku(metin)
|
||
if sinir and pencere.ogren(HOME, hedef, sinir):
|
||
log(f"pencere ogrenildi: {hedef} gercek siniri {sinir} token; model listesi yenileniyor")
|
||
threading.Thread(target=load_ide_catalog, daemon=True).start()
|
||
|
||
def _aracsiz_ogren(self, hedef):
|
||
if pencere.ogren_aracsiz(HOME, hedef):
|
||
log(f"aracsiz model ogrenildi: {hedef}; model listesinde isaretlenecek")
|
||
threading.Thread(target=load_ide_catalog, daemon=True).start()
|
||
|
||
def _send_sse_failed(self, kod, mesaj):
|
||
"""Codex'in akis icinde tanidigi hata bicimi (response.failed + error.code)."""
|
||
kimlik = "resp_astrabridge_%d" % int(time.time() * 1000)
|
||
olay = {"type": "response.failed", "response": {"id": kimlik, "object": "response", "status": "failed",
|
||
"error": {"code": kod, "message": mesaj}, "output": []}}
|
||
govde = b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n"
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "text/event-stream")
|
||
self.send_header("Content-Length", str(len(govde)))
|
||
self.end_headers()
|
||
self.wfile.write(govde)
|
||
|
||
def _send_json(self, status, obj):
|
||
b = json.dumps(obj).encode()
|
||
self.send_response(status)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Content-Length", str(len(b)))
|
||
self.end_headers()
|
||
self.wfile.write(b)
|
||
|
||
def _control(self, path):
|
||
"""Local status/stop for AITURK IDE. Loopback only; carries no secrets."""
|
||
if self.command == "POST" and path.rstrip("/").endswith("/refresh"):
|
||
# The IDE asks for this after a sign-in or a settings change, so the model
|
||
# list does not wait for the next periodic refresh.
|
||
try:
|
||
load_ide_catalog()
|
||
except Exception as e:
|
||
log(f"katalog yenileme hatasi: {e}")
|
||
path = "/aiturk/codex-baglama/status"
|
||
self.command = "GET"
|
||
if self.command == "GET" and path.rstrip("/").endswith("/status"):
|
||
durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ")
|
||
return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": durum,
|
||
"ide_model": int((sayi.split() or ["0"])[0]),
|
||
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
|
||
"is_hatti": bool(HAT_KOMUT[0]),
|
||
"adim_denetimi": bool(jev_denetim.anahtar())})
|
||
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
|
||
self._send_json(200, {"ok": True})
|
||
log("AITURK IDE istegiyle kapatiliyor")
|
||
threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start()
|
||
return
|
||
return self._send_json(404, {"error": {"message": "unknown control path"}})
|
||
|
||
def _handle(self):
|
||
with lock:
|
||
seq[0] += 1; n = seq[0]
|
||
if "websocket" in (self.headers.get("Upgrade") or "").lower():
|
||
log(f"[{n} ws-upgrade {self.path[:60]}] -> 426, HTTP'ye dusuruluyor")
|
||
self.send_response(426)
|
||
self.send_header("Content-Length", "0")
|
||
self.send_header("Connection", "close")
|
||
self.end_headers()
|
||
return
|
||
body = self._read()
|
||
path = self.path
|
||
if path.startswith("/aiturk/codex-baglama/"):
|
||
return self._control(path)
|
||
if DEBUG:
|
||
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
|
||
if self.command == "POST" and path.split("?", 1)[0].rstrip("/").endswith("/responses"):
|
||
return self._responses(n, path, body)
|
||
return self._passthrough(n, path, body)
|
||
|
||
def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None, izle=None):
|
||
tag = f"[{n} {tagname} {self.command} {path[:80]}]"
|
||
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP}
|
||
if rewritten:
|
||
hdrs = {k: v for k, v in hdrs.items() if k.lower() != "content-encoding"}
|
||
if self.command == "GET" and "/models" in path:
|
||
# The upstream validator knows nothing about the added 9Router catalog.
|
||
hdrs = {k: v for k, v in hdrs.items() if k.lower() not in ("if-none-match", "if-modified-since")}
|
||
req = urllib.request.Request(CHATGPT + path, data=body or None, headers=hdrs, method=self.command)
|
||
try:
|
||
resp = urllib.request.urlopen(req, timeout=UPSTREAM_IDLE_TIMEOUT); status = resp.status
|
||
except urllib.error.HTTPError as e:
|
||
resp = e; status = e.code
|
||
except Exception as e:
|
||
log(f"{tag} HATA {e}")
|
||
# Ag yokken de model secici bosalmasin: chatgpt.com'a hic ulasilamazsa son basarili liste verilir.
|
||
if self.command == "GET" and "/models" in path and self._son_listeyi_ver(tag, self.headers.get("chatgpt-account-id") or "-"):
|
||
return
|
||
return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
|
||
if self.command == "GET" and "/models" in path:
|
||
acct = self.headers.get("chatgpt-account-id") or "-"
|
||
if status == 200:
|
||
out = self._models(resp, tag)
|
||
return out
|
||
body = resp.read()[:300]
|
||
log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}")
|
||
if self._son_listeyi_ver(tag, acct):
|
||
return
|
||
self.send_response(status)
|
||
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
|
||
self.end_headers(); self.wfile.write(body); return
|
||
if status >= 400:
|
||
errbody = resp.read()
|
||
log(f"{tag} chatgpt.com {status}: {errbody[:300]!r}")
|
||
self.send_response(status)
|
||
for k, v in resp.headers.items():
|
||
if k.lower() in RESP_HOP: continue
|
||
self.send_header(k, v)
|
||
self.send_header("Content-Length", str(len(errbody)))
|
||
self.end_headers(); self.wfile.write(errbody); return
|
||
if (self.command == "POST" and "/realtime/calls" in path and status in (200, 201)
|
||
and (CFG.get("voice_prep") or {}).get("enabled")):
|
||
# Sesli on hazirlayici (bridge.json voice_prep.enabled). Hata atmaz; basliklar yalniz bellekte.
|
||
try:
|
||
import voice_prep
|
||
if voice_prep.baslat(resp.headers.get("Location"), hdrs, os.path.join(HOME, "bridge.json"), LOGDIR):
|
||
log(f"{tag} sesli on hazirlayici baslatildi")
|
||
except Exception as e:
|
||
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
|
||
self._stream(resp, status, tag, rewriter, izle)
|
||
|
||
def _son_listeyi_ver(self, tag, acct):
|
||
"""Bu hesabin son basarili model listesini gonderir. True: gonderildi."""
|
||
cached = LAST_MODELS.get(acct)
|
||
if not cached:
|
||
return False
|
||
self.send_response(200)
|
||
for k, v in cached["headers"]:
|
||
self.send_header(k, v)
|
||
self.send_header("Content-Length", str(len(cached["body"])))
|
||
self.end_headers(); self.wfile.write(cached["body"])
|
||
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
|
||
return True
|
||
|
||
def _openai_ozet_coz(self, n, oge, ozet_cfg):
|
||
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
|
||
klasor = os.path.join(LOGDIR, "ozet-onbellek")
|
||
metin = ozet_kopru.onbellekten(klasor, oge)
|
||
if metin:
|
||
return metin
|
||
govde = ozet_kopru.coz_istegi(oge, ozet_cfg.get("openai_modeli", "gpt-6-luna"))
|
||
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"}
|
||
hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream"
|
||
r = urllib.request.Request(CHATGPT + self.path, data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST")
|
||
try:
|
||
resp = urllib.request.urlopen(r, timeout=float(ozet_cfg.get("coz_zaman_asimi", 300)))
|
||
metin = ozet_kopru.sse_metni(resp.read())
|
||
except urllib.error.HTTPError as e:
|
||
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: HTTP {e.code} {e.read()[:200]!r}")
|
||
return None
|
||
except Exception as e:
|
||
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: {type(e).__name__}")
|
||
return None
|
||
if metin:
|
||
ozet_kopru.onbellege(klasor, oge, metin)
|
||
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)")
|
||
return metin or None
|
||
|
||
def _ozet_openai(self, n, path, req, slug, ozet_cfg, girdi_bayt):
|
||
"""Kopru modeline (ide/, 9r/) gelen sikistirma isteginin ozetini abonelik modeline yazdirir.
|
||
Donus: True = yanit istemciye gitti; False = abonelik istegi baslamadan basarisiz oldu
|
||
(istemciye hicbir sey yazilmadi, cagiran hedef modelle devam eder). req cagiranin kopyasidir."""
|
||
model = ozet_cfg.get("ozet_modeli", "gpt-6.1-sol")
|
||
tag = f"[{n} CHATGPT {model} <- {slug}]"
|
||
try:
|
||
req["model"] = model
|
||
fix_tool_ids(req)
|
||
atilan = drop_foreign_items(req)
|
||
ozet_kopru.ozetleri_ac(req, yabanci_hedef=False)
|
||
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
|
||
body = json.dumps(req, ensure_ascii=False).encode("utf-8")
|
||
except Exception as e:
|
||
log(f"{tag} ozet kopru: abonelik ozeti hazirlanamadi ({type(e).__name__}: {e}), hedef model deneniyor")
|
||
return False
|
||
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"}
|
||
hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream"
|
||
r = urllib.request.Request(CHATGPT + path, data=body, headers=hdrs, method="POST")
|
||
try:
|
||
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
|
||
except urllib.error.HTTPError as e:
|
||
log(f"{tag} ozet kopru: abonelik ozeti reddedildi (HTTP {e.code} {e.read()[:200]!r}), hedef model deneniyor")
|
||
return False
|
||
except Exception as e:
|
||
log(f"{tag} ozet kopru: abonelik ozeti baslatilamadi ({type(e).__name__}), hedef model deneniyor")
|
||
return False
|
||
log(f"{tag} ozet kopru: sikistirma ozeti abonelik modeline yazdiriliyor (girdi={len(req.get('input', []))} oge, "
|
||
f"{girdi_bayt} bayt, {atilan} yabanci oge atildi)")
|
||
akis = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
|
||
self._stream(resp, resp.status, tag, akis)
|
||
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
|
||
return True
|
||
|
||
def _models(self, resp, tag):
|
||
raw = resp.read()
|
||
try:
|
||
raw = decompress(raw, resp.headers.get("Content-Encoding"))
|
||
data = json.loads(raw)
|
||
models = data.get("models", [])
|
||
changed = 0
|
||
# Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz).
|
||
by_slug = {m.get("slug"): m for m in models}
|
||
ozgun_kimlikler = set(by_slug)
|
||
ekler = EXTRA_MODELS # tek kopya: liste hazirlanirken katalog yenilenirse sira ve icerik tutarli kalir
|
||
for x in ekler:
|
||
if x["slug"] in by_slug:
|
||
continue
|
||
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
|
||
m = copy.deepcopy(base)
|
||
m["slug"] = x["slug"]
|
||
m["display_name"] = x.get("display_name", x["slug"])
|
||
m["description"] = x.get("description", "")
|
||
m["visibility"] = "list"
|
||
m["priority"] = x.get("priority", 0)
|
||
m["upgrade"] = None; m["availability_nux"] = None
|
||
dis = x["slug"].startswith(IDE_PREFIX)
|
||
if not dis:
|
||
m["additional_speed_tiers"] = []; m["service_tiers"] = []
|
||
# Dis modelde hiz katmani ilan edilmeye devam eder (yoksa Codex her turda "service tier ... will be
|
||
# omitted" uyarisi basar); istekteki service_tier alani gecide gonderilmeden once silinir.
|
||
if dis and CFG.get("dis_arac_modu", "direct") == "direct":
|
||
# Abonelik kalibi "code_mode_only": tek arac JavaScript calistiran `exec`. Zayif modeller bunu
|
||
# cagiramiyor ya da ciktiyi yazdirmiyor (10 Ekim 2026 olcumu). Dis modellere Codex'in klasik
|
||
# araclari dogrudan verilir (GPT-5.5 kalibindaki gibi); Responses Lite kapatilir.
|
||
m["tool_mode"] = None
|
||
m["use_responses_lite"] = False
|
||
m["experimental_supported_tools"] = []
|
||
if x.get("context_window"):
|
||
cw = int(x["context_window"])
|
||
m["context_window"] = cw; m["max_context_window"] = cw
|
||
if x.get("reasoning_levels"):
|
||
m["supported_reasoning_levels"] = [l for l in m.get("supported_reasoning_levels", []) if l.get("effort") in x["reasoning_levels"]]
|
||
if SLIM_CLONES:
|
||
saved = {f_: m[f_] for f_ in SLIM_FIELDS if f_ in m}
|
||
for f_ in SLIM_FIELDS:
|
||
m.pop(f_, None)
|
||
# Codex, bir girdide hem base_instructions hem
|
||
# model_messages.instructions_template yoksa listenin TAMAMINI
|
||
# reddeder. Kirpma sonrasi ikisi de kalmadiysa geri koy.
|
||
mm = m.get("model_messages")
|
||
has_tmpl = isinstance(mm, dict) and mm.get("instructions_template")
|
||
if not m.get("base_instructions") and not has_tmpl:
|
||
m.update(saved)
|
||
models.append(m); changed += 1
|
||
if ekler:
|
||
ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler)
|
||
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||
except Exception as e:
|
||
log(f"{tag} model listesi degistirilemedi: {e}")
|
||
out = raw; changed = 0
|
||
raw_len = len(out)
|
||
duz = out # sikistirilmamis govde: yedege bu yazilir
|
||
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
|
||
enc_out = None
|
||
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
|
||
import gzip
|
||
out = gzip.compress(out, compresslevel=6); enc_out = "gzip"
|
||
hdrs_out = []
|
||
for k, v in resp.headers.items():
|
||
kl = k.lower()
|
||
if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue
|
||
if kl in ("etag", "x-models-etag") and changed:
|
||
v = catalog_etag
|
||
hdrs_out.append((k, v))
|
||
if changed:
|
||
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
|
||
acct_ = self.headers.get("chatgpt-account-id") or "-"
|
||
onceki = LAST_MODELS.get(acct_)
|
||
LAST_MODELS[acct_] = {"headers": list(hdrs_out), "body": duz, "at": time.strftime("%Y-%m-%d %H:%M:%S")}
|
||
# Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz.
|
||
if not onceki or onceki.get("body") != duz:
|
||
save_last_models(acct_, list(hdrs_out), duz)
|
||
if enc_out:
|
||
hdrs_out.append(("Content-Encoding", enc_out))
|
||
self.send_response(200)
|
||
for k, v in hdrs_out:
|
||
self.send_header(k, v)
|
||
self.send_header("Content-Length", str(len(out)))
|
||
self.end_headers()
|
||
self.wfile.write(out)
|
||
log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi")
|
||
|
||
def _yerel_yanit(self, slug, metin):
|
||
govde = hazir_yaniti(slug, metin)
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "text/event-stream")
|
||
self.send_header("Content-Length", str(len(govde)))
|
||
self.end_headers()
|
||
self.wfile.write(govde)
|
||
|
||
def _model_komutu(self, n, req, sohbet, gelen):
|
||
"""Son kullanici mesaji "modeli X yap" / "hangi model" ise isler. Yerel cevap metni doner;
|
||
None: istek (gerekirse degistirilmis olarak) modele gider."""
|
||
metin, sira, sesli = model_gecis.son_kullanici_metni(req)
|
||
komut = model_gecis.komut_coz(metin) if metin else None
|
||
if not komut:
|
||
return None
|
||
etkin = yonlendirilen(sohbet, gelen) or gelen
|
||
ortada = model_gecis.tur_ortasi(req, sira)
|
||
if komut["tur"] == "sor":
|
||
if ortada:
|
||
return None
|
||
log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}")
|
||
return f"Sohbetin modeli şu an {model_adi(etkin)}."
|
||
if komut["tur"] == "nasil":
|
||
if ortada:
|
||
return None
|
||
log(f"[{n} {gelen}] adsiz model komutu: nasil soylenecegi anlatildi")
|
||
return f"Sohbetin modeli şu an {model_adi(etkin)}. " + model_gecis.NASIL
|
||
# Sesle gecis yalniz bizim ekledigimiz modeller arasindadir; Codex'in kendi modelleri aday degildir
|
||
# (kullanici karari, 11 Ekim 2026). Iki taraf arasinda gecis: ekrandaki seciciden model secilir, "." yazilir.
|
||
bilinen = bilinen_modeller()
|
||
sonuc = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if ek_model_mu(s)])
|
||
if not sonuc:
|
||
sonuc = model_gecis.eslestir(komut["ad"], gecit_modelleri()) # Codex listesinde olmayan gecit modeli
|
||
if not sonuc:
|
||
ozgun = model_gecis.eslestir(komut["ad"], [(s, a) for s, a in bilinen if not ek_model_mu(s)])
|
||
if ozgun and (komut.get("kesin", True) or "slug" in ozgun):
|
||
kim = f"{ozgun['ad']} Codex'in kendi modeli" if "ad" in ozgun else "Bu ad Codex'in kendi modellerine uyuyor"
|
||
ileti = (f"{kim}; Codex'in kendi modellerine sesle geçilmiyor, model değişmedi. Geçmek için ekrandaki "
|
||
f"model seçiciden o modeli seç ve tek başına \".\" yaz. Sohbetin modeli hâlâ {model_adi(etkin)}.")
|
||
log(f"[{n} {gelen}] model komutu: Codex'in kendi modeli istendi {komut['ad']}, gecilmedi")
|
||
if not ortada:
|
||
return ileti
|
||
if not komut.get("kesin", True):
|
||
return None
|
||
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı Codex'in kendi modeline geçmek istedi; bu sesle "
|
||
"yapılamıyor, model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + ileti + "]")
|
||
self.komut_govdeyi_degistirdi = True
|
||
return None
|
||
if not komut.get("kesin", True) and not (sonuc and "slug" in sonuc):
|
||
return None # "modeli" denmemis ve ad tek modele oturmuyor: komut degil, modele gider
|
||
if not sonuc:
|
||
if ortada:
|
||
return None
|
||
# Acik komut ("modeli ... yap") ama ad bulunamadi: model "kopru yapacak" diye yanlis soz vermesin.
|
||
log(f"[{n} {gelen}] model komutu: ad bulunamadi {komut['ad']}")
|
||
return (f"\"{' '.join(komut['ad'])}\" adına uyan bir model bulamadım, model değişmedi; sohbetin modeli hâlâ "
|
||
f"{model_adi(etkin)}. Adını biraz farklı söyler misin?")
|
||
if "adaylar" in sonuc:
|
||
soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: "
|
||
+ ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?")
|
||
log(f"[{n} {gelen}] model komutu belirsiz: {komut['ad']} -> {sonuc['adaylar']}")
|
||
if not ortada:
|
||
return soru
|
||
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; "
|
||
"model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]")
|
||
self.komut_govdeyi_degistirdi = True
|
||
return None
|
||
hedef = sonuc["slug"]
|
||
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
|
||
return "AITURK bağlantısı şu an kapalı; bu modele geçemedim, model değişmedi."
|
||
YONLENDIRME.koy(sohbet, gelen, hedef)
|
||
log(f"[{n} {gelen}] model komutu: {model_adi(etkin)} -> {sonuc['ad']}"
|
||
f" ({'tur ortasi, is devrediliyor' if ortada else 'bosta'}{', sesli' if sesli else ''})")
|
||
if hedef == etkin:
|
||
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
|
||
if ortada:
|
||
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
|
||
self.komut_govdeyi_degistirdi = True
|
||
return None
|
||
# Listede ayni modeli yeniden secmek yonlendirmeyi kaldirmaz (Codex ayni adi gonderir); geri donus yolu soylenir.
|
||
donus = f" Listede seçili modele dönmek için \"modeli {model_adi(gelen)} yap\" de." if hedef != gelen else ""
|
||
return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. "
|
||
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}")
|
||
|
||
def _ozgun(self, n, path, body, req, slug):
|
||
"""Codex'in kendi (abonelik) modeli: kopru HICBIR SEY yapmaz, istek chatgpt.com'a bayt bayt oldugu gibi gider.
|
||
Kullanici karari (11 Ekim 2026): "Codex kendi modelini kullanirken kopruyle bir bagi olmamali ... Codex'in
|
||
kendi modeline dokunulmamali" ve "Codex'i secip Codex'e yaziyorsan bizim kopru yardim amacli bile yaninda
|
||
olamaz." Bu yuzden model komutu, nokta karsilama, bilgi notu, is hatti talimati, yarim cevap notu,
|
||
sikistirmaya karisma ve koprunun kendi izlerini temizleme de YOKTUR; sikistirmayi Codex kendisi yapar
|
||
(olcum 11 Ekim 2026: kopru ozgun modelde ozeti kendi uretmeye calisip Codex'i "Error running remote compact
|
||
task" ile takti).
|
||
Bilinen sonuc: ayni sohbette daha once ek model kullanildiysa ya da kopru ozeti kaldiysa chatgpt.com bu
|
||
istegi reddedebilir; o sohbet ek modellerle surer, Codex'in kendi modeli icin yeni sohbet acilir."""
|
||
return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=False)
|
||
|
||
def _responses(self, n, path, body):
|
||
try:
|
||
req = json.loads(decompress(body, self.headers.get("Content-Encoding")))
|
||
except Exception as e:
|
||
log(f"[{n} responses] govde cozulemedi: {e}")
|
||
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
|
||
slug = req.get("model", "")
|
||
gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model)
|
||
if not ek_model_mu(gelen):
|
||
return self._ozgun(n, path, body, req, slug)
|
||
govde_degisti = False # istek govdesi kopruce degistirildi: abonelik yolunda yeniden yazilir
|
||
once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)
|
||
sohbet = str(req.get("prompt_cache_key") or "")
|
||
if CFG.get("sesle_model", True) and sohbet:
|
||
self.komut_govdeyi_degistirdi = False
|
||
try:
|
||
yerel = self._model_komutu(n, req, sohbet, gelen)
|
||
except Exception as e:
|
||
yerel = None
|
||
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
|
||
if yerel is not None:
|
||
return self._yerel_yanit(gelen, yerel)
|
||
if self.komut_govdeyi_degistirdi:
|
||
govde_degisti = True # komut mesaji nota cevrildi: abonelik yolunda da degismis govde gider
|
||
try:
|
||
if model_gecis.gecmisi_temizle(req, bilinen_modeller(), yonlendirilen(sohbet, gelen) or gelen):
|
||
govde_degisti = True
|
||
except Exception as e:
|
||
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
|
||
hedef = yonlendirilen(sohbet, gelen)
|
||
if hedef and hedef != gelen:
|
||
req["model"] = slug = hedef
|
||
log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor")
|
||
mapping = MODEL_MAP.get(slug)
|
||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
||
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
||
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
|
||
del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir
|
||
govde_degisti = True
|
||
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
|
||
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
|
||
govde_degisti = True
|
||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req):
|
||
# Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da
|
||
# istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur.
|
||
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
|
||
return self._yerel_yanit(gelen, f"{model_adi(slug)} seçildi. Sohbetin modeli artık {model_adi(slug)}.")
|
||
# Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py).
|
||
kaynak = self.headers.get("originator") or ""
|
||
if kaynak and kaynak not in GORULEN_ISTEMCI:
|
||
GORULEN_ISTEMCI.add(kaynak)
|
||
log(f"istemci goruldu: {kaynak}")
|
||
try:
|
||
# Model, sohbetin modelinin soylenerek degistirilebildigini bilsin; yoksa "degistiremem" diyor.
|
||
if CFG.get("sesle_model", True) and CFG.get("model_bilgi", True) and sohbet and model_gecis.bilgi_ekle(req):
|
||
govde_degisti = True
|
||
if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle(
|
||
req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)):
|
||
govde_degisti = True
|
||
# Ek modelde yerlesik web aramasi yok (flatten_request atar): aramayi kabukla yapan komut soylenir.
|
||
if internet.ekle(req, internet.talimat(CFG.get("internet"), os.path.join(HERE, "ara.py"))):
|
||
govde_degisti = True
|
||
except Exception as e:
|
||
log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}")
|
||
# Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir.
|
||
yarim_eklenen = 0
|
||
if CFG.get("yarim_cevap", True):
|
||
try:
|
||
yarim_eklenen = yarim.uygula(req, sohbet)
|
||
except Exception as e:
|
||
log(f"[{n} {slug}] yarim cevap notu atlandi: {type(e).__name__}: {e}")
|
||
if yarim_eklenen:
|
||
log(f"[{n} {slug}] yarim cevap notu eklendi ({yarim_eklenen} karakter)")
|
||
izle = yarim.Izleyici(sohbet, slug) if (sohbet and CFG.get("yarim_cevap", True)) else None
|
||
try:
|
||
if not ozet_kopru.tetik_mi(req):
|
||
OZET_FRENI.normal(sohbet or slug) # ozet olmayan istek: art arda ozet sayaci sifirlanir
|
||
except Exception:
|
||
pass
|
||
ozet_cfg = CFG.get("ozet_kopru") or {}
|
||
ozet_acik = ozet_cfg.get("enabled", False)
|
||
catalog = CFG.get("nine_router_catalog") or {}
|
||
prefix = catalog.get("prefix", "9r/")
|
||
if mapping is None and prefix and slug.startswith(prefix):
|
||
if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES:
|
||
# Newly added API models can be selected before the background refresh.
|
||
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
|
||
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
|
||
else:
|
||
# Rotasi kalmamis eski ad chatgpt.com'a baska bir adla gonderilmez ("model desteklenmiyor" donuyordu).
|
||
log(f"[{n} {slug}] bu on ekin rotasi yok; istek gonderilmedi")
|
||
return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}})
|
||
ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler
|
||
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
|
||
mapping = (ide_route, slug[len(ide_prefix):], None)
|
||
if mapping is None and slug.startswith(ide_prefix):
|
||
# Never send an IDE model to chatgpt.com under another name.
|
||
log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok")
|
||
# 503 donmek Codex'i sessizce yeniden denetiyor ve tur cevapsiz bitiyordu; sesli asistan da "bakiyorum"
|
||
# deyip duruyordu (olcum 11 Ekim 2026, sunucu gecisi sirasinda). Kullanici durumu duysun.
|
||
if CFG.get("kesinti_mesaji", True):
|
||
return self._yerel_yanit(gelen, "AITURK modellerine şu an ulaşılamıyor (AITURK sunucusu cevap vermiyor ya da "
|
||
"bağlantı kapalı); bu model şimdi cevap veremez. Beklemek istemezsen ekrandaki model "
|
||
"seçiciden Codex'in kendi modellerinden birini seç ve tek başına \".\" yaz.")
|
||
return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}})
|
||
if mapping is None:
|
||
# Codex'in kendi modelleri en basta _ozgun ile ayrildi; buraya yalniz rotasi cozulemeyen bir ek ad duser.
|
||
# Ek model chatgpt.com'a baska bir adla gonderilmez.
|
||
log(f"[{n} {slug}] ek modelin rotasi cozulemedi; istek gonderilmedi")
|
||
return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}})
|
||
route_name, target, flat = mapping
|
||
try:
|
||
ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req)
|
||
except Exception as e:
|
||
ozet_tetik = False; ozet_acik = False
|
||
log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}")
|
||
girdi_bayt = 0
|
||
if ozet_tetik:
|
||
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3))
|
||
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
|
||
log(f"[{n} {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)")
|
||
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
|
||
"type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}})
|
||
girdi_bayt = len(json.dumps(req.get("input") or [], ensure_ascii=False).encode("utf-8"))
|
||
if ozet_tetik and ozet_cfg.get("model"):
|
||
# Ozeti baglami alabilen baska bir modele yazdirmak icin (ornegin kucuk hedef kendi basina ozetleyemiyorsa).
|
||
alt = MODEL_MAP.get(ozet_cfg["model"])
|
||
if alt:
|
||
route_name, target, flat = alt
|
||
elif ozet_tetik and ozet_cfg.get("ozetleyen", "openai") == "openai":
|
||
# Ozeti secili kopru modeli degil, abonelikteki guclu model yazar (10 Ekim 2026: 1 MB'lik
|
||
# sohbeti kucuk bir model 2201 karaktere indirdi ve model degisince hafiza gitti).
|
||
# Abonelik istegi baslamadan reddedilirse asagidaki eski yol (hedef model) calisir.
|
||
if self._ozet_openai(n, path, copy.deepcopy(req), slug, ozet_cfg, girdi_bayt):
|
||
return
|
||
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
|
||
via_router = route_name in (catalog.get("route", "9router"), ide_route)
|
||
route = ROUTES.get(route_name)
|
||
if route is None:
|
||
# Katalog yenilemesi rotayi bu istek hazirlanirken kaldirdi (baglama kapandi ya da IDE oturumu gitti).
|
||
log(f"[{n} {slug}] rota artik yok: {route_name}")
|
||
return self._send_json(503, {"error": {"message": "Bu modelin baglantisi su an kapali; birazdan yeniden deneyin ya da baska bir model secin."}})
|
||
tag = f"[{n} {route_name.upper()} {slug}->{target}]"
|
||
req["model"] = target
|
||
req.pop("client_metadata", None)
|
||
if route_name == ide_route:
|
||
req.pop("service_tier", None) # abonelik hiz katmani; gecit tanimaz
|
||
for k in route.get("strip_fields", []):
|
||
req.pop(k, None)
|
||
if ozet_acik:
|
||
ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True, coz=lambda oge: self._openai_ozet_coz(n, oge, ozet_cfg))
|
||
if ob["acilan"] or ob["cozulen"] or ob["okunamayan"]:
|
||
log(f"{tag} ozet kopru: kopru ozeti={ob['acilan']}, OpenAI ozeti duz metne cevrildi={ob['cozulen']}, okunamayan={ob['okunamayan']}")
|
||
if ozet_tetik:
|
||
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
|
||
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
|
||
if via_router and CFG.get('resim_sadelestir', True):
|
||
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
|
||
# Duzlestirmeden ONCE: kimliksiz arac ciktisi orada duz metne cevrilir ve icindeki resim taninmaz olur.
|
||
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
|
||
if cikan:
|
||
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
|
||
rewriter = None
|
||
if flat if flat is not None else route.get("flatten_tools"):
|
||
custom = flatten_request(req)
|
||
duzelen = arac_semalarini_duzelt(req)
|
||
if duzelen:
|
||
log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)")
|
||
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
|
||
fix_tool_ids(req)
|
||
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
|
||
merged = coalesce_context(req)
|
||
if merged:
|
||
log(f"{tag} Gemini instruction context: {merged} message(s) preserved")
|
||
elif via_router and CFG.get('pano_kanca_user', True):
|
||
# 9Router Claude cevirisi developer mesajlarini dusuruyor; bu turun PANO OLAYLARI blogu (sesli_pano.py)
|
||
# sona user mesaji olarak eklenir (kanca_context.py, 24 Eylul 2026). bridge.json "pano_kanca_user": false kapatir.
|
||
eklenen, ozet = pano_blogunu_ulastir(req)
|
||
if eklenen:
|
||
log(f"{tag} PANO OLAYLARI blogu user mesaji olarak eklendi ({eklenen} karakter, {ozet})")
|
||
if via_router:
|
||
# Claude "assistant message prefill" desteklemiyor (25 Eylul 2026, sesli tur kapaninca 400).
|
||
eklendi, sonlar = claude_sonu_duzelt(req)
|
||
if eklendi:
|
||
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
|
||
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
|
||
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
|
||
# Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir).
|
||
pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere)
|
||
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
|
||
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude"))))
|
||
if rewriter is not None:
|
||
rewriter.dolu = tahmin >= hedef_pencere * 0.7
|
||
if kirpilan:
|
||
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
|
||
if not sigdi:
|
||
log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})")
|
||
return self._send_sse_failed("context_length_exceeded",
|
||
f"Bu mesaj seçili modelin bağlam penceresine sığmıyor (yaklaşık {tahmin} token, pencere {hedef_pencere}). "
|
||
"Daha büyük pencereli bir model seçin ya da mesajı kısaltın.")
|
||
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
|
||
log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt")
|
||
try:
|
||
# Istegin bicimi (icerik degil): gecidin reddettigi istegi sonradan ayirt edebilmek icin.
|
||
son = (req.get("input") or [None])[-1]
|
||
bicim = {k: v for k, v in req.items() if k not in ("input", "tools", "instructions", "prompt_cache_key")}
|
||
log(f"{tag} bicim={json.dumps(bicim, ensure_ascii=False)[:420]} son_oge="
|
||
f"{(son.get('type'), son.get('role')) if isinstance(son, dict) else type(son).__name__}"
|
||
+ (f" icerik={json.dumps(son, ensure_ascii=False)[:700]}" if isinstance(son, dict) and son.get("type") not in
|
||
("message", "function_call_output", "custom_tool_call_output", "function_call", "reasoning") else ""))
|
||
except Exception:
|
||
pass
|
||
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
|
||
# Katman denetimi (jev_denetim.py): kopruye ugrayan HER ek modelin cevabi Jev'e sorulur; gecmezse adim
|
||
# sirayla Katman-1 -> 2 -> 3 -> 4 yapar (secili model zaten katmansa ondan sonrakiler). Katman-4 sorulmadan
|
||
# gecer. Katmanlar OmniRoute combolaridir: yedek her zaman AITURK gecidi (ide_route) uzerinden acilir,
|
||
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
|
||
# (rewriter yok) yolda kapalidir.
|
||
jev_rota = ROUTES.get(ide_route)
|
||
jev_sira = ([] if ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar()
|
||
else jev_denetim.sonrakiler(target))
|
||
jev_istek = jev_denetim.son_istek(req) if jev_sira else ""
|
||
|
||
def jev_ac(ad):
|
||
katman_req = json.loads(data)
|
||
katman_req["model"] = ad
|
||
jev_hdrs = {"Authorization": f"Bearer {jev_rota['key']}", "Content-Type": "application/json",
|
||
"Accept": "text/event-stream", "User-Agent": UA}
|
||
yanit = urllib.request.urlopen(urllib.request.Request(
|
||
jev_rota["url"], data=json.dumps(katman_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"),
|
||
timeout=UPSTREAM_IDLE_TIMEOUT)
|
||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren),
|
||
f"[{n} {slug}=>{ad}]")
|
||
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
|
||
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
|
||
# only failures before the work starts (no connection, 500/502/503/529) are retried.
|
||
combo = "/" not in target
|
||
for attempt in range(1, RETRIES + 1):
|
||
r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST")
|
||
try:
|
||
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
|
||
except urllib.error.HTTPError as e:
|
||
errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir
|
||
last = (e.code, errbody[:400])
|
||
etxt = errbody.decode("utf-8", "replace")
|
||
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
|
||
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
|
||
permanent = (any(p.lower() in etxt.lower() for p in NO_RETRY_PATTERNS)
|
||
or bool(re.search(r"\[4(?!29|08)\d\d\]", etxt)))
|
||
if permanent:
|
||
log(f"{tag} kalici hata, tekrar denenmiyor: HTTP {e.code} {errbody[:160]!r}")
|
||
# 524 = Cloudflare gave up waiting while the origin kept working; a retry
|
||
# re-runs (and re-bills) the whole Fusion (22 Sep 2026: 3 double runs).
|
||
elif e.code in ((500, 502, 503, 529) if combo else (500, 502, 503, 504, 529)) and attempt < RETRIES:
|
||
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
|
||
time.sleep(min(2 * attempt, 8)); continue
|
||
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
|
||
if jev_sira: # secili model hata verdi: adimi siradaki katman yapar
|
||
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac)
|
||
if pencere.tasma_mi(etxt):
|
||
# Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin
|
||
# bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur).
|
||
self._tasma_ogren(target, etxt)
|
||
return self._send_sse_failed("context_length_exceeded", etxt[:300])
|
||
self.send_response(e.code)
|
||
self.send_header("Content-Type", "application/json")
|
||
bekle = e.headers.get("Retry-After") if e.headers is not None else None
|
||
if bekle:
|
||
self.send_header("Retry-After", str(bekle)) # 429'da ne kadar beklenecegi istemciye ulassin
|
||
self.send_header("Content-Length", str(len(errbody)))
|
||
self.end_headers(); self.wfile.write(errbody); return
|
||
except Exception as e:
|
||
last = (0, str(e))
|
||
if combo and is_timeout(e):
|
||
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
|
||
break
|
||
if combo and kopma_mi(e):
|
||
log(f"{tag} combo istegi gonderildikten sonra baglanti koptu, tekrar gonderilmiyor (calisiyor olabilir): {e}")
|
||
break
|
||
log(f"{tag} deneme {attempt}: {e}")
|
||
time.sleep(min(2 * attempt, 8)); continue
|
||
# Headers go to the client from here on: a cut stream is never resent.
|
||
if ozet_tetik:
|
||
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
|
||
sonuc = self._stream(resp, resp.status, tag, akis)
|
||
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
|
||
return sonuc
|
||
if jev_sira and resp.status == 200:
|
||
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac)
|
||
return self._stream(resp, resp.status, tag, rewriter, izle)
|
||
if jev_sira: # secili modele ulasilamadi: adimi siradaki katman yapar
|
||
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac)
|
||
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()
|