feat(codex-bridge): ship the local Codex bridge with the IDE

Adds the IDE's models and combos to Codex's model list and keeps conversation
summaries readable across providers, so switching models in Codex no longer
drops the conversation. Standard-library Python, run on the agent venv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-09 15:46:10 +03:00
1 parent df63432c92
commit 1ce5ab82cf
13 files changed
+2071

No files matched your search

@@ -0,0 +1,21 @@
# AITURK IDE Codex bridge
Local HTTP proxy that AITURK IDE's **Settings → Codex Bağlama** runs for OpenAI Codex
(desktop or CLI) on the same computer. Standard-library Python only; it runs on the
agent venv's interpreter and keeps running after the IDE window closes, because
Codex depends on it while `openai_base_url` points here.
- `bridge.py`: the proxy. Subscription models go to chatgpt.com unchanged; `ide/<id>`
models go to the AITURK gateway configured in the agent's `config.yaml`, with the
key read from the agent's `.env` on every catalog refresh (never written elsewhere).
- `ide_catalog.py`: which IDE models and combos are added to Codex's model list and
how they are named (`provider/model`, combos by bare name).
- `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made
by one provider stays readable after the user switches to another.
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE writes
`<data dir>/bridge.json` and reads `GET /aiturk/codex-baglama/status`;
`POST /aiturk/codex-baglama/stop` ends the process.
Tests: `python -m unittest discover` in this directory.
+914
View File
@@ -0,0 +1,914 @@
"""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
SURUM = "1"
HERE = os.path.dirname(os.path.abspath(__file__))
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]
def load_ide_catalog():
"""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)
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")
# Secicide yalniz "saglayici/model" gorunur. 9Router listesinde ayni adla zaten duran model
# ikinci kez eklenmez (ayni modelin iki satir olmamasi icin).
var_olan = {m.get("display_name", "").lower() for m in extra_models}
for g in girdiler:
slug = IDE_PREFIX + g["id"]
g["kisa"] = ide_catalog.kisa_ad(g["ad"])
# "ad (2)": ayni modelin gecitteki ikinci yolu; taban ad listede varsa o da eklenmez.
girdiler = [g for g in girdiler if re.sub(r" \(\d+\)$", "", g["kisa"]).lower() not in var_olan]
adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan)
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": f"AITURK IDE: {g['ad']}", "route": IDE_ROUTE, "target": g["id"],
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels})
model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1
else:
ROUTES.pop(IDE_ROUTE, None)
MODEL_MAP = model_map
EXTRA_MODELS = extra_models
ozet = f"{durum}, {n} model"
if IDE_DURUM[0] != ozet: # 5 dakikada bir ayni satiri yazmamak icin
IDE_DURUM[0] = ozet
log(f"AITURK IDE Codex Baglama: {ozet}")
load_ide_catalog()
def catalog_refresh_loop():
"""Kataloglari arka planda yeniler; liste istegini yavaslatmaz."""
minutes = max(1, int(CFG.get("catalog_refresh_min", 5)))
while True:
time.sleep(minutes * 60)
for yukle in (load_9router_catalog, load_ide_catalog):
try:
yukle()
except Exception as e:
log(f"katalog yenileme hatasi: {e}")
threading.Thread(target=catalog_refresh_loop, daemon=True).start()
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
def _cache_path(acct):
guvenli = re.sub(r"[^A-Za-z0-9._-]", "_", acct or "-")[:64]
return os.path.join(CACHE_DIR, f"models-{guvenli}.json")
def save_last_models(acct, hdrs, body):
"""Son basarili listeyi diske yazar. Kopru yeniden baslayinca bellek
sifirlandigi icin, restarttan sonraki ilk 401 listeyi silip goturuyordu."""
try:
os.makedirs(CACHE_DIR, exist_ok=True)
yol = _cache_path(acct)
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
"body": body.decode("utf-8")}
gecici = yol + ".tmp"
with open(gecici, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol) # yarim dosya birakmamak icin
except Exception as e:
log(f"son liste diske yazilamadi: {e}")
def load_last_models():
"""Acilista diskteki son listeleri bellege alir."""
try:
if not os.path.isdir(CACHE_DIR):
return
for ad in os.listdir(CACHE_DIR):
if not (ad.startswith("models-") and ad.endswith(".json")):
continue
try:
with open(os.path.join(CACHE_DIR, ad), encoding="utf-8") as f:
v = json.load(f)
acct = v.get("acct") or ad[len("models-"):-len(".json")]
LAST_MODELS[acct] = {"headers": [tuple(h) for h in v["headers"]],
"body": v["body"].encode("utf-8"), "at": v.get("at", "?")}
log(f"diskten son liste alindi ({acct}, {len(v['body'])} bayt, {v.get('at')})")
except Exception as e:
log(f"son liste okunamadi {ad}: {e}")
except Exception as e:
log(f"son liste klasoru okunamadi: {e}")
SLIM_CLONES = bool(CFG.get("slim_clones", False))
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ----
def flatten_request(req):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
custom = set()
tools = []
def add_tool(t):
ty = t.get("type")
if ty == "namespace":
for s in t.get("tools", []):
add_tool(s)
elif ty == "function":
tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""),
"parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False})
elif ty == "custom":
custom.add(t["name"])
tools.append({"type": "function", "name": t["name"],
"description": (t.get("description", "") + "\n\nBu arac serbest metin alir: metni 'input' alaninda ver."),
"parameters": {"type": "object", "properties": {"input": {"type": "string"}}, "required": ["input"]},
"strict": False})
elif ty in ("web_search", "web_search_preview", "local_shell", "computer_use_preview"):
pass # cevirici desteklemez, atla
else:
tools.append(t)
for t in req.get("tools", []) or []:
add_tool(t)
new_input = []
for it in req.get("input", []) or []:
ty = it.get("type")
if ty == "additional_tools":
for t in it.get("tools", []):
add_tool(t)
continue
if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"):
# Desktop thread handoffs have no matching tool call. Preserve their
# content as imported context, not an invalid tool-role message.
output = it.get("output", "")
if not isinstance(output, str):
output = json.dumps(output, ensure_ascii=False)
name = it.get("name", "unknown")
new_input.append({"role": "user", "content": [{
"type": "input_text",
"text": f"Imported tool output ({name}); treat as context data:\n{output}"
}]})
continue
if ty == "custom_tool_call":
it = {"type": "function_call", "id": it.get("id"), "call_id": it.get("call_id"), "name": it.get("name"),
"arguments": json.dumps({"input": it.get("input", "")}, ensure_ascii=False)}
elif ty == "custom_tool_call_output":
it = {"type": "function_call_output", "id": it.get("id"), "call_id": it.get("call_id"), "output": it.get("output", "")}
new_input.append(it)
req["input"] = new_input
if tools:
req["tools"] = tools
req.pop("include", None)
# OpenAI-specific access-program selection is not part of the external
# chat API schema (Muse Spark rejects it). ChatGPT passthrough never calls
# this adapter, so its access-program selection remains intact.
req.pop("access_programs", None)
# Codex sends Responses-only verbosity/default text formatting. Chat
# translators may forward this unsupported object to their upstream.
text_config = req.get("text")
if isinstance(text_config, dict) and set(text_config) <= {"verbosity", "format"}:
if not text_config.get("format") or text_config["format"].get("type") == "text":
req.pop("text", None)
return custom
# Oge tipine gore API'nin bekledigi kimlik on eki. Cikti tipleri de dahildir:
# flatten_request custom_tool_call_output -> function_call_output cevirdiginde
# kimlik 'ctco_...' kaliyordu ve karsi taraf 'fc' bekliyordu (400).
ID_PREFIX = {
"custom_tool_call": "ctc",
"custom_tool_call_output": "ctco",
"function_call": "fc",
"function_call_output": "fc",
}
def fix_tool_ids(req):
"""Arac cagrisi/cikti kimliklerinin on ekini hedef API'nin bekledigine cevirir.
Codex gecmisinde kalan yanlis on ekli kimlikler (or. fc_toolu_..., ctco_...)
400 hatasina yol aciyordu."""
changed = 0
for it in req.get("input", []) or []:
if not isinstance(it, dict):
continue
want = ID_PREFIX.get(it.get("type"))
i = it.get("id")
if not want or not isinstance(i, str) or not i:
continue
# Alt cizgi ile karsilastir: 'ctco_x'.startswith('ctc') dogru dondugu icin
# ctco kimlikleri yanlislikla "zaten dogru" sayiliyordu.
if i.startswith(want + "_"):
continue
tail = i.split("_", 1)[1] if "_" in i else i
it["id"] = want + "_" + tail
changed += 1
return changed
# Cevirici uretimi kimliklerin izi. 9Router chat.completions'a cevirdigi icin
# uydurdugu kimliklerde bu parca bulunur; chatgpt.com boyle bir kaydi saklamaz.
FOREIGN_ID_MARK = "chatcmpl-"
# Icerigini tasimayan, yalnizca hedefteki kayda ISARET EDEN oge tipleri.
# Kimlik yabanciysa hedef cozemez; ceviriyle kurtarilamaz, atilmalari gerekir.
REFERENCE_TYPES = ("reasoning", "item_reference")
# Icerigini kendisi tasiyan tipler: yabanci kimlik tasisalar da sorun degil,
# uyari uretmezler. ID_PREFIX'teki tipler de bu gruba dahildir.
INLINE_TYPES = ("message",)
def drop_foreign_items(req):
"""Baska bir saglayicinin urettigi, hedefin cozemeyecegi ogeleri atar.
Ilke hermes-agent'tan: sahipli kimligi tasima. hermes gecmisi saglayicidan
bagimsiz tuttugu icin model degistirince hicbir sey kirilmiyor; Codex ise
Responses bicimindeki sahipli kimlikleri tasiyor. Model degistirildiginde
chatgpt.com 404 'Item ... not found. Items are not persisted when store is
set to false' diyor ve tur hic baslamiyordu.
Icerigini iceren ogeler (function_call vb.) atilmaz; onlarin kimligi
fix_tool_ids ile duzeltilir. Yalnizca isaret eden tipler atilir."""
inp = req.get("input")
if not isinstance(inp, list):
return 0
kalan, atilan, bilinmeyen = [], 0, set()
for it in inp:
i = it.get("id") if isinstance(it, dict) else None
if isinstance(i, str) and FOREIGN_ID_MARK in i:
ty = it.get("type")
if ty in REFERENCE_TYPES:
atilan += 1
continue
if ty not in ID_PREFIX and ty not in INLINE_TYPES:
# Tanimadigimiz bir tip yabanci kimlik tasiyor. Simdilik
# gecirilir; hata verirse gunlukte hangi tip oldugu yazili olur.
bilinmeyen.add(ty)
kalan.append(it)
if atilan:
req["input"] = kalan
if bilinmeyen:
log(f"UYARI: yabanci kimlikli taninmayan oge tipleri gecirildi: {sorted(bilinmeyen)}")
return atilan
class SseRewriter:
"""function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE."""
def __init__(self, custom):
self.custom = custom
self.buf = b""
self.custom_ids = set()
def _fix(self, d):
t = d.get("type", "")
item = d.get("item")
if item and item.get("type") == "function_call" and item.get("name") in self.custom:
self.custom_ids.add(item.get("id"))
args = item.get("arguments") or ""
try:
inp = json.loads(args).get("input", "") if args else ""
except Exception:
inp = args
d["item"] = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": item.get("status", "completed")}
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"}
out.append(item)
d["response"]["output"] = out
return d
def feed(self, chunk):
self.buf += chunk
out = b""
while b"\n\n" in self.buf:
block, self.buf = self.buf.split(b"\n\n", 1)
lines = block.split(b"\n")
data_idx = [i for i, l in enumerate(lines) if l.startswith(b"data:")]
if not data_idx:
out += block + b"\n\n"; continue
try:
d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx))
except Exception:
out += block + b"\n\n"; continue
d = self._fix(d)
if d is None:
continue
ev = [l for l in lines if l.startswith(b"event:")]
evline = (b"event: " + d.get("type", "").encode() + b"\n") if ev else b""
out += evline + b"data: " + json.dumps(d, ensure_ascii=False).encode("utf-8") + b"\n\n"
return out
def flush(self):
b, self.buf = self.buf, b""
return b
RESP_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"}
REQ_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"}
def claude_sonu_duzelt(req):
"""9Router Claude cevirisi developer/system mesajlarini dusurur; geriye kalan son oge asistan mesaji ya da
reasoning ise Claude 'assistant message prefill' hatasi (400) verir. O durumda sona kisa user mesaji eklenir.
Doner: (eklendi_mi, son ogelerin kisa ozeti)."""
items = req.get("input")
if not isinstance(items, list):
return False, ""
ozet = []
for it in reversed(items):
if not isinstance(it, dict):
continue
tip, rol = it.get("type", "message"), it.get("role")
ozet.append(f"{tip}/{rol}" if rol else tip)
if tip == "message" and rol in ("developer", "system"):
continue
if (tip == "message" and rol == "assistant") or tip == "reasoning":
items.append({"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "(Önceki cevabın yarıda kaldı ya da sesli konuşma kapandı; bağlama göre devam et.)"}]})
return True, ",".join(ozet[:3])
return False, ""
return False, ""
def decompress(body, enc):
enc = (enc or "").lower().strip()
if enc == "gzip":
import gzip; return gzip.decompress(body)
if enc == "deflate":
import zlib; return zlib.decompress(body)
if enc == "zstd":
try:
from compression import zstd as _z; return _z.decompress(body)
except ImportError:
import zstandard; return zstandard.ZstdDecompressor().decompressobj().decompress(body)
if enc == "br":
import brotli; return brotli.decompress(body)
return body
OZET_FRENI = ozet_kopru.DonguFreni()
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a): pass
def _read(self):
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
out = bytearray()
while True:
line = self.rfile.readline().strip()
if not line:
continue
size = int(line.split(b";")[0], 16)
if size == 0:
while self.rfile.readline().strip():
pass
break
out += self.rfile.read(size)
self.rfile.readline()
return bytes(out)
n = int(self.headers.get("Content-Length") or 0)
return self.rfile.read(n) if n else b""
def _stream(self, resp, status, tag, rewriter=None):
self.send_response(status)
for k, v in resp.headers.items():
if k.lower() in RESP_HOP: continue
self.send_header(k, v)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
total = 0
def w(b):
if b:
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
try:
while True:
chunk = resp.read(4096)
if not chunk: break
total += len(chunk)
w(rewriter.feed(chunk) if rewriter else chunk)
if rewriter: w(rewriter.flush())
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
log(f"{tag} istemci baglantiyi kapatti ({total} bayt)")
log(f"{tag} bitti status={status} {total} bayt")
def _send_json(self, status, obj):
b = json.dumps(obj).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b)))
self.end_headers()
self.wfile.write(b)
def _control(self, path):
"""Local status/stop for AITURK IDE. Loopback only; carries no secrets."""
if self.command == "GET" and path.rstrip("/").endswith("/status"):
durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ")
return self._send_json(200, {"ok": True, "surum": SURUM, "ide_durum": durum,
"ide_model": int((sayi.split() or ["0"])[0]),
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False))})
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
self._send_json(200, {"ok": True})
log("AITURK IDE istegiyle kapatiliyor")
threading.Thread(target=lambda: (time.sleep(0.3), os._exit(0)), daemon=True).start()
return
return self._send_json(404, {"error": {"message": "unknown control path"}})
def _handle(self):
with lock:
seq[0] += 1; n = seq[0]
if "websocket" in (self.headers.get("Upgrade") or "").lower():
log(f"[{n} ws-upgrade {self.path[:60]}] -> 426, HTTP'ye dusuruluyor")
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
return
body = self._read()
path = self.path
if path.startswith("/aiturk/codex-baglama/"):
return self._control(path)
if DEBUG:
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
if self.command == "POST" and path.rstrip("/").endswith("/responses"):
return self._responses(n, path, body)
return self._passthrough(n, path, body)
def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None):
tag = f"[{n} {tagname} {self.command} {path[:80]}]"
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP}
if rewritten:
hdrs = {k: v for k, v in hdrs.items() if k.lower() != "content-encoding"}
if self.command == "GET" and "/models" in path:
# The upstream validator knows nothing about the added 9Router catalog.
hdrs = {k: v for k, v in hdrs.items() if k.lower() not in ("if-none-match", "if-modified-since")}
req = urllib.request.Request(CHATGPT + path, data=body or None, headers=hdrs, method=self.command)
try:
resp = urllib.request.urlopen(req, timeout=UPSTREAM_IDLE_TIMEOUT); status = resp.status
except urllib.error.HTTPError as e:
resp = e; status = e.code
except Exception as e:
log(f"{tag} HATA {e}"); return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
if self.command == "GET" and "/models" in path:
acct = self.headers.get("chatgpt-account-id") or "-"
tok = (self.headers.get("Authorization") or "")[-8:]
if status == 200:
out = self._models(resp, tag)
return out
body = resp.read()[:300]
log(f"{tag} chatgpt.com {status} verdi (hesap={acct} token=..{tok}) govde={body[:160]!r}")
cached = LAST_MODELS.get(acct)
if cached:
self.send_response(200)
for k, v in cached["headers"]:
self.send_header(k, v)
self.send_header("Content-Length", str(len(cached["body"])))
self.end_headers(); self.wfile.write(cached["body"])
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
return
self.send_response(status)
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
self.end_headers(); self.wfile.write(body); return
if status >= 400:
errbody = resp.read()
log(f"{tag} chatgpt.com {status}: {errbody[:300]!r}")
self.send_response(status)
for k, v in resp.headers.items():
if k.lower() in RESP_HOP: continue
self.send_header(k, v)
self.send_header("Content-Length", str(len(errbody)))
self.end_headers(); self.wfile.write(errbody); return
if (self.command == "POST" and "/realtime/calls" in path and status in (200, 201)
and (CFG.get("voice_prep") or {}).get("enabled")):
# Sesli on hazirlayici (bridge.json voice_prep.enabled). Hata atmaz; basliklar yalniz bellekte.
try:
import voice_prep
if voice_prep.baslat(resp.headers.get("Location"), hdrs, os.path.join(HOME, "bridge.json"), LOGDIR):
log(f"{tag} sesli on hazirlayici baslatildi")
except Exception as e:
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
self._stream(resp, status, tag, rewriter)
def _openai_ozet_coz(self, n, oge, ozet_cfg):
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
klasor = os.path.join(LOGDIR, "ozet-onbellek")
metin = ozet_kopru.onbellekten(klasor, oge)
if metin:
return metin
govde = ozet_kopru.coz_istegi(oge, ozet_cfg.get("openai_modeli", "gpt-6-luna"))
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"}
hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream"
r = urllib.request.Request(CHATGPT + self.path, data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST")
try:
resp = urllib.request.urlopen(r, timeout=float(ozet_cfg.get("coz_zaman_asimi", 300)))
metin = ozet_kopru.sse_metni(resp.read())
except urllib.error.HTTPError as e:
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: HTTP {e.code} {e.read()[:200]!r}")
return None
except Exception as e:
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrilemedi: {type(e).__name__}")
return None
if metin:
ozet_kopru.onbellege(klasor, oge, metin)
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)")
return metin or None
def _models(self, resp, tag):
raw = resp.read()
try:
raw = decompress(raw, resp.headers.get("Content-Encoding"))
data = json.loads(raw)
models = data.get("models", [])
changed = 0
for m in models:
new = DISPLAY_MAP.get(m.get("slug"))
if new:
m["display_name"] = new; changed += 1
by_slug = {m.get("slug"): m for m in models}
for x in EXTRA_MODELS:
if x["slug"] in by_slug:
continue
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
m = copy.deepcopy(base)
m["slug"] = x["slug"]
m["display_name"] = x.get("display_name", x["slug"])
m["description"] = x.get("description", "")
m["visibility"] = "list"
m["priority"] = x.get("priority", 0)
m["upgrade"] = None; m["availability_nux"] = None
m["additional_speed_tiers"] = []; m["service_tiers"] = []
if x.get("context_window"):
cw = int(x["context_window"])
m["context_window"] = cw; m["max_context_window"] = cw
if x.get("reasoning_levels"):
m["supported_reasoning_levels"] = [l for l in m.get("supported_reasoning_levels", []) if l.get("effort") in x["reasoning_levels"]]
if SLIM_CLONES:
saved = {f_: m[f_] for f_ in SLIM_FIELDS if f_ in m}
for f_ in SLIM_FIELDS:
m.pop(f_, None)
# Codex, bir girdide hem base_instructions hem
# model_messages.instructions_template yoksa listenin TAMAMINI
# reddeder. Kirpma sonrasi ikisi de kalmadiysa geri koy.
mm = m.get("model_messages")
has_tmpl = isinstance(mm, dict) and mm.get("instructions_template")
if not m.get("base_instructions") and not has_tmpl:
m.update(saved)
models.append(m); changed += 1
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}")
out = raw; changed = 0
raw_len = len(out)
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
enc_out = None
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
import gzip
out = gzip.compress(out, compresslevel=6); enc_out = "gzip"
hdrs_out = []
for k, v in resp.headers.items():
kl = k.lower()
if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue
if kl in ("etag", "x-models-etag") and changed:
v = catalog_etag
hdrs_out.append((k, v))
if enc_out:
hdrs_out.append(("Content-Encoding", enc_out))
if changed and not enc_out:
acct_ = self.headers.get("chatgpt-account-id") or "-"
onceki = LAST_MODELS.get(acct_)
LAST_MODELS[acct_] = {"headers": hdrs_out, "body": out, "at": time.strftime("%H:%M:%S")}
# Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz.
if not onceki or onceki.get("body") != out:
save_last_models(acct_, hdrs_out, out)
self.send_response(200)
for k, v in hdrs_out:
self.send_header(k, v)
self.send_header("Content-Length", str(len(out)))
self.end_headers()
self.wfile.write(out)
log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi")
def _responses(self, n, path, body):
try:
req = json.loads(decompress(body, self.headers.get("Content-Encoding")))
except Exception as e:
log(f"[{n} responses] govde cozulemedi: {e}")
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
slug = req.get("model", "")
mapping = MODEL_MAP.get(slug)
ozet_cfg = CFG.get("ozet_kopru") or {}
ozet_acik = ozet_cfg.get("enabled", False)
catalog = CFG.get("nine_router_catalog") or {}
prefix = catalog.get("prefix", "9r/")
if mapping is None and catalog.get("enabled") and slug.startswith(prefix):
# Newly added API models can be selected before the background refresh.
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
mapping = (ide_route, slug[len(ide_prefix):], None)
if mapping is None and slug.startswith(ide_prefix):
# Never send an IDE model to chatgpt.com under another name.
log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok")
return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}})
if mapping is None:
# abonelik modeli: chatgpt.com'a oldugu gibi
fixed = fix_tool_ids(req)
atilan = drop_foreign_items(req)
degisti = False
ozet_akisi = None
if ozet_acik:
# Ozet bagdastirici (ozet_kopru.py; 9 Ekim 2026'da yalniz bu parca geri acildi): koprunun urettigi
# duz metin ozetleri OpenAI modeline de mesaj olarak gider.
try:
ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=False)
# kapsam "hepsi": OpenAI modelinde de ozeti kopru DUZ METIN urettirir; boylece ozet sonradan
# secilen her saglayicida okunur. kapsam "9router": OpenAI ozeti yerlesik (sifreli) kalir.
if ozet_cfg.get("kapsam", "hepsi") == "hepsi" and ozet_kopru.tetik_mi(req):
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"[{n} CHATGPT {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
"type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}})
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
ozet_akisi = ozet_kopru.OzetAkisi(None, tohum=str(n))
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 {'uretildi, ' + str(len(ozet_akisi.metin)) + ' karakter' if ozet_akisi.metin else 'URETILEMEDI'}")
return sonuc
return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}",
rewritten=bool(fixed or atilan or degisti))
route_name, target, flat = mapping
try:
ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req)
except Exception as e:
ozet_tetik = False; ozet_acik = False
log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}")
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
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
via_router = route_name in (catalog.get("route", "9router"), ide_route)
route = ROUTES[route_name]
tag = f"[{n} {route_name.upper()} {slug}->{target}]"
req["model"] = target
req.pop("client_metadata", None)
for k in route.get("strip_fields", []):
req.pop(k, None)
if ozet_acik:
ob = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True, coz=lambda oge: self._openai_ozet_coz(n, oge, ozet_cfg))
if ob["acilan"] or ob["cozulen"] or ob["okunamayan"]:
log(f"{tag} ozet kopru: kopru ozeti={ob['acilan']}, OpenAI ozeti duz metne cevrildi={ob['cozulen']}, okunamayan={ob['okunamayan']}")
if ozet_tetik:
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"{tag} 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"}})
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
rewriter = SseRewriter(custom)
fix_tool_ids(req)
if via_router and CFG.get('resim_sadelestir', True):
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
cikan, kazanc = resimleri_sadelestir(req)
if cikan:
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
merged = coalesce_context(req)
if merged:
log(f"{tag} Gemini instruction context: {merged} message(s) preserved")
elif via_router and CFG.get('pano_kanca_user', True):
# 9Router Claude cevirisi developer mesajlarini dusuruyor; bu turun PANO OLAYLARI blogu (sesli_pano.py)
# sona user mesaji olarak eklenir (kanca_context.py, 24 Eylul 2026). bridge.json "pano_kanca_user": false kapatir.
eklenen, ozet = pano_blogunu_ulastir(req)
if eklenen:
log(f"{tag} PANO OLAYLARI blogu user mesaji olarak eklendi ({eklenen} karakter, {ozet})")
if via_router:
# Claude "assistant message prefill" desteklemiyor (25 Eylul 2026, sesli tur kapaninca 400).
eklendi, sonlar = claude_sonu_duzelt(req)
if eklendi:
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt")
if DEBUG:
open(os.path.join(LOGDIR, f"{n:04d}_req.json"), "wb").write(data)
hdrs = {"Authorization": f"Bearer {route['key']}", "Content-Type": "application/json", "Accept": "text/event-stream", "User-Agent": UA}
last = None
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
# only failures before the work starts (no connection, 500/502/503/529) are retried.
combo = "/" not in target
for attempt in range(1, RETRIES + 1):
r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST")
try:
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
except urllib.error.HTTPError as e:
errbody = e.read()[:400]
last = (e.code, errbody)
etxt = errbody.decode("utf-8", "replace")
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
permanent = (any(p.lower() in etxt.lower() for p in NO_RETRY_PATTERNS)
or bool(re.search(r"\[4(?!29|08)\d\d\]", etxt)))
if permanent:
log(f"{tag} kalici hata, tekrar denenmiyor: HTTP {e.code} {errbody[:160]!r}")
# 524 = Cloudflare gave up waiting while the origin kept working; a retry
# re-runs (and re-bills) the whole Fusion (22 Sep 2026: 3 double runs).
elif e.code in ((500, 502, 503, 529) if combo else (500, 502, 503, 504, 529)) and attempt < RETRIES:
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
time.sleep(min(2 * attempt, 8)); continue
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
self.send_response(e.code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(errbody)))
self.end_headers(); self.wfile.write(errbody); return
except Exception as e:
last = (0, str(e))
if combo and is_timeout(e):
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
break
log(f"{tag} deneme {attempt}: {e}")
time.sleep(min(2 * attempt, 8)); continue
# Headers go to the client from here on: a cut stream is never resent.
if ozet_tetik:
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n))
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: ozet {'uretildi, ' + str(len(akis.metin)) + ' karakter' if akis.metin else 'URETILEMEDI'}")
return sonuc
return self._stream(resp, resp.status, tag, rewriter)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
do_POST = _handle
do_GET = _handle
do_PUT = _handle
do_DELETE = _handle
do_PATCH = _handle
class TS(http.server.ThreadingHTTPServer):
daemon_threads = True
# Windows'ta SO_REUSEADDR baska bir surecin ayni portu "calmasina" izin verir.
# Kapali: port doluysa sessizce yan yana dinlemek yerine acikca hata verir.
allow_reuse_address = False
if __name__ == "__main__":
load_last_models()
log(f"AstraBridge :{PORT} routes={list(ROUTES)} model_map={len(MODEL_MAP)} diger->{CHATGPT}")
try:
srv = TS(("127.0.0.1", PORT), H)
except OSError as e:
log(f"HATA: 127.0.0.1:{PORT} baglanamadi ({e}). Port baska bir surecte olabilir; "
f"bridge.json icindeki 'port' degerini ve config.toml'daki openai_base_url'i degistirin.")
sys.exit(1)
srv.serve_forever()
@@ -0,0 +1,33 @@
"""Compatibility for Gemini translators that retain only the last system turn.
Only instruction-bearing text messages are folded; user/tool content is never
promoted. Caller must scope this to the affected Gemini route. No network or IO.
"""
def coalesce_context(request):
items = request.get('input')
base = request.get('instructions', '')
if not isinstance(items, list) or not isinstance(base, str):
return 0
chunks = ['[BASE SYSTEM INSTRUCTIONS]\n' + base] if base else []
remaining = []
count = 0
for item in items:
if not isinstance(item, dict) or item.get('type', 'message') != 'message' or item.get('role') not in ('system', 'developer'):
remaining.append(item)
continue
content = item.get('content', '')
if isinstance(content, str):
text = content
elif isinstance(content, list) and all(isinstance(p, dict) and p.get('type') in ('input_text','text') and isinstance(p.get('text'), str) for p in content):
text = '\n'.join(p['text'] for p in content)
else:
# Unknown multimodal instruction block: do not discard or rewrite.
return 0
chunks.append('[' + item['role'].upper() + ' CONTEXT ' + str(count+1) + ']\n' + text)
count += 1
if count:
request['instructions'] = '\n\n'.join(chunks)
request['input'] = remaining
return count
@@ -0,0 +1,150 @@
"""AITURK IDE "Codex Baglama": IDE'nin model ve combolarini Codex listesine ekler.
Kaynak, IDE'nin kullandigi gecittir (config.yaml -> providers.turkservis.base_url);
anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazilmaz.
Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json
{"enabled": true, "kapsam": "ide"}
Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik).
kapsam "ide" : IDE ayarinda tanimli modeller + adlandirilmis combolar (varsayilan)
kapsam "hepsi" : gecitteki butun sohbet modelleri (1000+; Codex listesi cok buyur)
"""
import json, os, re, urllib.request
# Agent home: HERMES_HOME when the IDE passes it, else the default install location.
AGENT = os.environ.get("HERMES_HOME") or os.path.join(os.environ.get("LOCALAPPDATA", ""), "TurkServis", "AITURK-IDE", "agent")
IDE_DIR = os.path.dirname(os.path.normpath(AGENT))
AYAR = os.path.join(IDE_DIR, "codex-baglama.json")
KEY_NAME = "HERMES_CUSTOM_TURKSERVIS_API_KEY"
DEFAULT_BASE = "https://ai.turkservis.online/v1"
def ayar_oku(yol=AYAR):
"""Dosya yoksa ya da bozuksa varsayilan: acik, kapsam 'ide'."""
ayar = {"enabled": True, "kapsam": "ide"}
try:
with open(yol, encoding="utf-8") as f:
v = json.load(f)
if isinstance(v, dict):
if isinstance(v.get("enabled"), bool):
ayar["enabled"] = v["enabled"]
if v.get("kapsam") in ("ide", "hepsi"):
ayar["kapsam"] = v["kapsam"]
except (OSError, ValueError):
pass
return ayar
def anahtar_oku(agent=AGENT):
try:
with open(os.path.join(agent, ".env"), encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith(KEY_NAME + "="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return ""
def ide_yapilandirma(agent=AGENT):
"""config.yaml'dan gecit adresi, varsayilan model ve tanimli model adlari.
Kopru bagimliliksiz calistigi icin YAML'in yalniz bu bolumu satir satir okunur."""
base, varsayilan, adlar = DEFAULT_BASE, "", []
try:
with open(os.path.join(agent, "config.yaml"), encoding="utf-8", errors="replace") as f:
lines = f.read().splitlines()
except OSError:
return base, varsayilan, adlar
bolum = saglayici = None
modellerde = False
for line in lines:
if not line.strip() or line.lstrip().startswith("#"):
continue
girinti = len(line) - len(line.lstrip(" "))
metin = line.strip()
if girinti == 0:
bolum = metin.rstrip(":"); saglayici = None; modellerde = False
continue
if bolum == "model" and girinti == 2:
if metin.startswith("default:"):
varsayilan = metin.split(":", 1)[1].strip().strip("'\"")
continue
if bolum != "providers":
continue
if girinti == 2:
saglayici = metin.rstrip(":"); modellerde = False
elif saglayici == "turkservis" and girinti == 4:
modellerde = metin == "models:"
if metin.startswith("base_url:"):
base = metin.split(":", 1)[1].strip().strip("'\"") or base
elif saglayici == "turkservis" and modellerde and girinti == 6:
ad = re.split(r":\s*(\{|$)", metin)[0].strip().strip("'\"")
if ad:
adlar.append(ad)
return base.rstrip("/"), varsayilan, adlar
def kisa_ad(ad):
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b",
"OmniRoute · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash", combo adi oldugu gibi kalir."""
ad = (ad or "").split(" · ")[-1].strip()
parcalar = [p for p in ad.split("/") if p]
if len(parcalar) <= 2:
return "/".join(parcalar) or ad
return parcalar[0] + "/" + parcalar[-1]
def adlandir(girdiler, dolu=()):
"""[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur;
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar."""
kisa = {k: kisa_ad(a) for k, a in girdiler}
tam = {k: (a or "").split(" · ")[-1].strip() for k, a in girdiler}
sayac = {}
for v in list(kisa.values()) + list(dolu):
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
sonuc, kullanilan = {}, {d.lower() for d in dolu}
for k, _ in girdiler:
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
if ad.lower() in kullanilan:
ad = f"{ad} [{k[-8:]}]"
kullanilan.add(ad.lower())
sonuc[k] = ad
return sonuc
def sec(data, tanimli, kapsam="ide"):
"""Gecit katalogundan Codex'e eklenecek girdiler: [{'id','ad'}], sirali ve tekrarsiz.
Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez."""
tanimli = {a.lower() for a in tanimli if a}
secilen, gorulen = [], set()
for m in data:
mid = m.get("id") or ""
ts = m.get("turkservis") or {}
if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]):
continue
gorunen = ts.get("display_name") or mid
kisa = gorunen.split(" · ")[-1].strip()
combo = "/" not in kisa # saglayici/model bicimi degil: adlandirilmis combo ya da takma ad
if kapsam != "hepsi" and not combo and kisa.lower() not in tanimli and mid.lower() not in tanimli:
continue
gorulen.add(mid)
secilen.append({"id": mid, "ad": gorunen, "sira": ts.get("order") or 0})
secilen.sort(key=lambda s: (s["sira"], s["id"]))
return [{"id": s["id"], "ad": s["ad"]} for s in secilen]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
Ag hatasinda istisna atar; cagiran son listeyi korur."""
ayar = ayar_oku()
if not ayar["enabled"]:
return "kapali", "", "", []
anahtar = anahtar_oku()
if not anahtar:
return "anahtar-yok", "", "", []
base, varsayilan, adlar = ide_yapilandirma()
req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
data = json.load(urllib.request.urlopen(req, timeout=timeout)).get("data", [])
return "tamam", base, anahtar, sec(data, adlar + [varsayilan], ayar["kapsam"])
@@ -0,0 +1,269 @@
"""Sikistirma (ozet) bagdastiricisi (6 Ekim 2026, kart a-grubu-bakim-20261006-0417).
Codex 0.160.0 "remote compaction v2" kullanir: girdinin sonuna {"type": "compaction_trigger"} koyar ve yanitta tam bir
{"type": "compaction", "encrypted_content": ...} ogesi bekler. Gercek olcum (raporlar/sesli-kopru-kanit-20261006/sik1, sik2):
* 9Router modelleri bu ogeyi uretemez -> "remote compaction v2 expected exactly one compaction output item, got 0",
tur hata verir (kucuk modele geciste ve 9Router modelinde esik asilinca).
* OpenAI'nin urettigi sifreli ozet 9Router modelince okunamaz -> kararlar/gerekceler kaybolur.
Bu modul:
1. 9Router'a giden sikistirma isteginde ozeti duz metin olarak ayni (ya da bridge.json ozet_kopru.model ile secilen)
modele yazdirir ve Codex'in bekledigi "compaction" ogesine sarar (encrypted_content = ISARET + base64(metin)).
2. Koprunun urettigi ozet ogelerini her giden istekte duz metin kullanici mesajina acar (hangi saglayiciya giderse).
3. OpenAI'nin sifreli ozeti 9Router modeline gidecekse, OpenAI'ye bir kez "bu ozeti duz metne dok" diye sorar,
sonucu diske onbellekler ve onu gonderir; olmazsa durust bir not koyar (sessiz kayip yok).
OpenAI -> OpenAI sikistirmasi oldugu gibi (yerlesik) kalir.
Kapatmak: bridge.json "ozet_kopru": {"enabled": false}
"""
import base64, hashlib, json, os, re
ISARET = "ASTRABRIDGE-OZET-1:"
OZET_BASLIK = "<onceki_konusma_ozeti>"
OZET_BITIS = "</onceki_konusma_ozeti>"
OZET_ISTEMI = """BAGLAM KONTROL NOKTASI (bu mesaj kullanicidan degil, kopru altyapisindan): Bu konusma baglam sinirina yaklastigi icin ozetlenip baska bir modele devredilecek. Simdi arac CAGIRMA; yalniz asagidaki ozeti yaz. Ozeti okuyacak model bu konusmayi goremeyecek, yalniz senin yazdigini gorecek; bu yuzden kisa tutmak icin bilgi atma. Bu ozet istegini kullanicinin istegi gibi ozetleme.
Kurallar:
- Kullanicinin ve konusmanin kullandigi etiket, kimlik ve adlari (ornek: "KARAR-1", kart/is numaralari, dosya adlari, model adlari) HARFI HARFINE koru ve her bilgiyi kendi etiketiyle yaz.
- Sayilari, tarihleri, yollari, komutlari ve onemli ozgun cumleleri degistirmeden aktar.
- Uydurma; emin olmadigin seyi "dogrulanmadi" diye isaretle. Sifre, anahtar ve gizli degerleri yazma.
Su basliklarla, madde madde ve somut yaz:
1. KULLANICININ ISTEGI VE HEDEF: kullanicinin kendi sozleriyle ne istedigi, kabul olcutleri, kisitlar.
2. KARARLAR VE GEREKCELERI: verilen her karar ve neden verildigi (etiketleriyle); reddedilen secenekler ve nedeni.
3. YAPILANLAR VE ONEMLI SONUCLAR: degisen dosyalar, calistirilan komutlar ve gercek ciktilari, test/olcum sonuclari, dogrulanan ve dogrulanamayan seyler.
4. ACIK ISLER VE SIRADAKI ADIM: bitmemis isler, bekleyen sorular/onaylar, bir sonraki somut adim.
5. KULLANICI TERCIHLERI VE UYARILAR: kalici talimatlar, yapilmamasi istenenler, bilinen tuzaklar ve hatalar.
6. GEREKLI BASVURULAR: yollar, adresler, kart/is numaralari, model ve ayar adlari."""
COZ_ISTEMI = ("Yukaridaki sikistirilmis konusma ozetinin icerigini, bu konusmayi hic gormemis baska bir modelin isi "
"surdurebilmesi icin eksiksiz ve ayrintili DUZ METIN olarak yaz: kullanicinin istegi, kararlar ve gerekceleri, "
"yapilanlar ve onemli sonuclar, acik isler ve siradaki adim, kullanici tercihleri, gerekli yollar/kimlikler. "
"Arac cagirma, yorum ekleme, uydurma. Sifre ve anahtar yazma.")
OKUNAMADI_NOTU = ("(Kopru notu: Bu noktadan onceki konusma, onceki modelin sifreli ozetinde duruyor ve bu modele "
"aktarilamadi. Onceki kararlari bilmiyorsan uydurma; kullaniciya sor.)")
def tetik_mi(req):
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
ogeler = req.get("input")
return (isinstance(ogeler, list) and bool(ogeler) and isinstance(ogeler[-1], dict)
and ogeler[-1].get("type") == "compaction_trigger")
def ozet_ogesi(metin, kimlik_tohumu=""):
"""Duz metin ozeti Codex'in bekledigi compaction ogesine sarar."""
kimlik = "cmp_astrabridge_" + hashlib.sha256((kimlik_tohumu + metin).encode("utf-8")).hexdigest()[:32]
return {"type": "compaction", "id": kimlik,
"encrypted_content": ISARET + base64.b64encode(metin.encode("utf-8")).decode("ascii")}
def ozet_metni(oge):
"""Koprunun urettigi compaction ogesinden duz metni cikarir; kopru ogesi degilse None."""
icerik = oge.get("encrypted_content") if isinstance(oge, dict) else None
if not isinstance(icerik, str) or not icerik.startswith(ISARET):
return None
try:
return base64.b64decode(icerik[len(ISARET):]).decode("utf-8")
except Exception:
return None
def _ozet_mesaji(metin):
return {"type": "message", "role": "user",
"content": [{"type": "input_text", "text": OZET_BASLIK + "\n" + metin.strip() + "\n" + OZET_BITIS}]}
def ozetleri_ac(req, yabanci_hedef=False, coz=None):
"""Giden istekteki compaction ogelerini hedefin okuyabilecegi bicime getirir (yerinde).
- Koprunun urettigi ozet -> duz metin kullanici mesaji (her hedefte).
- Baska saglayicinin (OpenAI) sifreli ozeti: hedef OpenAI ise dokunulmaz; hedef yabanciysa (9Router) coz(oge)
ile duz metne cevrilir, olmazsa OKUNAMADI_NOTU konur.
Doner: {'acilan': n, 'cozulen': n, 'okunamayan': n}"""
bilgi = {"acilan": 0, "cozulen": 0, "okunamayan": 0}
ogeler = req.get("input")
if not isinstance(ogeler, list):
return bilgi
yeni = []
for oge in ogeler:
if isinstance(oge, dict) and oge.get("type") == "compaction":
metin = ozet_metni(oge)
if metin is not None:
yeni.append(_ozet_mesaji(metin)); bilgi["acilan"] += 1
continue
if yabanci_hedef:
cozulmus = None
if coz is not None:
try:
cozulmus = coz(oge)
except Exception:
cozulmus = None
if cozulmus:
yeni.append(_ozet_mesaji(cozulmus)); bilgi["cozulen"] += 1
else:
yeni.append({"type": "message", "role": "user",
"content": [{"type": "input_text", "text": OKUNAMADI_NOTU}]}); bilgi["okunamayan"] += 1
continue
yeni.append(oge)
if bilgi["acilan"] or bilgi["cozulen"] or bilgi["okunamayan"]:
req["input"] = yeni
return bilgi
def ozet_istegi_hazirla(req, istem=None):
"""Sikistirma istegini (sonunda compaction_trigger) duz metin ozet istegine cevirir (yerinde)."""
ogeler = [o for o in req.get("input") or [] if not (isinstance(o, dict) and o.get("type") == "compaction_trigger")]
ogeler.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": istem or OZET_ISTEMI}]})
req["input"] = ogeler
req["tool_choice"] = "none"
req["stream"] = True
return req
def _olay(d):
return b"event: " + d["type"].encode() + b"\ndata: " + json.dumps(d, ensure_ascii=False).encode("utf-8") + b"\n\n"
class OzetAkisi:
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
ic: once calisan cevirici (SseRewriter). Metin gelmezse compaction ogesi uretmez (Codex hatayi gosterir)."""
def __init__(self, ic=None, tohum=""):
self.ic = ic
self.tohum = tohum
self.buf = b""
self.parcalar = {} # item_id -> delta metni
self.bitenler = [] # tamamlanan mesaj metinleri
self.metin = None # sonuc (gunluk/test icin)
self.bitti = False
def _topla(self):
metin = "\n\n".join(t for t in self.bitenler if t.strip())
if not metin.strip():
metin = "\n\n".join(t for t in self.parcalar.values() if t.strip())
return metin.strip()
def _blok(self, blok):
veri = [s[5:].strip() for s in re.split(rb"\r?\n", blok) if s.startswith(b"data:")]
if not veri:
return b""
try:
d = json.loads(b"\n".join(veri))
except Exception:
return b""
if not isinstance(d, dict):
return b""
t = d.get("type", "")
if t == "response.created":
return _olay(d)
if t == "response.output_text.delta":
self.parcalar[d.get("item_id")] = self.parcalar.get(d.get("item_id"), "") + (d.get("delta") or "")
return b": ozet\n\n" # baglantiyi canli tutar; Codex yorum satirini yok sayar
if t == "response.output_item.done":
oge = d.get("item") or {}
if oge.get("type") == "message":
self.bitenler.append("".join(p.get("text", "") for p in oge.get("content") or []
if isinstance(p, dict) and p.get("type") == "output_text"))
return b": ozet\n\n"
if t == "response.completed":
self.metin = self._topla()
self.bitti = True
if not self.metin:
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
oge = ozet_ogesi(self.metin, self.tohum)
yanit = dict(d.get("response") or {})
yanit["output"] = [oge]
return (_olay({"type": "response.output_item.done", "output_index": 0, "item": oge})
+ _olay({"type": "response.completed", "response": yanit}))
if t in ("response.failed", "response.incomplete", "error"):
return _olay(d)
return b""
def feed(self, chunk):
if self.ic:
chunk = self.ic.feed(chunk)
self.buf += chunk
out = b""
while True:
m = re.search(rb"\r?\n\r?\n", self.buf)
if not m:
break
blok, self.buf = self.buf[:m.start()], self.buf[m.end():]
out += self._blok(blok)
return out
def flush(self):
if self.ic:
self.buf += self.ic.flush()
self.buf = b""
return b""
# ---- OpenAI'nin sifreli ozetini duz metne cevirme (onbellekli) ----
def onbellek_yolu(klasor, oge):
kimlik = str(oge.get("id") or hashlib.sha256(str(oge.get("encrypted_content")).encode()).hexdigest()[:32])
return os.path.join(klasor, re.sub(r"[^A-Za-z0-9_.-]", "_", kimlik)[:80] + ".txt")
def onbellekten(klasor, oge):
try:
with open(onbellek_yolu(klasor, oge), encoding="utf-8") as f:
return f.read().strip() or None
except OSError:
return None
def onbellege(klasor, oge, metin):
os.makedirs(klasor, exist_ok=True)
yol = onbellek_yolu(klasor, oge)
with open(yol + ".tmp", "w", encoding="utf-8") as f:
f.write(metin)
os.replace(yol + ".tmp", yol)
def coz_istegi(oge, model, talimat="Sen bir konusma ozeti aktaricisisin."):
"""Sifreli ozeti duz metne dokturmek icin OpenAI'ye gidecek istek govdesi."""
temiz = {k: v for k, v in oge.items() if k in ("type", "id", "encrypted_content")}
return {"model": model, "instructions": talimat, "store": False, "stream": True, "tools": [],
"tool_choice": "none", "parallel_tool_calls": False,
"reasoning": {"effort": "low", "context": "all_turns"}, # chatgpt.com arka ucu bunu sart kosuyor
"include": ["reasoning.encrypted_content"],
"input": [temiz, {"type": "message", "role": "user", "content": [{"type": "input_text", "text": COZ_ISTEMI}]}]}
def sse_metni(govde):
"""SSE govdesinden (bayt) asistan metnini toplar."""
a = OzetAkisi()
a.feed(govde if govde.endswith(b"\n\n") else govde + b"\n\n")
return a.metin if a.bitti else a._topla()
# ---- Dongu freni ----
# Ozetten sonra da baglam hedefin esigini asiyorsa (Codex son kullanici mesajlarini aynen tuttugu icin olabilir) Codex
# ayni turda tekrar tekrar sikistirma ister; her biri basarili oldugu icin dongu kendiliginden bitmez (olcum: sik6).
DONGU_MESAJI = ("AstraBridge: Bu sohbetin baglami, ozetlendikten sonra da secili modelin sinirina sigmiyor "
"(kisa surede %d ozet denendi). Dongu durduruldu. Daha buyuk baglam pencereli bir model secin ya da "
"yeni sohbet acin; mesajlariniz sohbette duruyor.")
class DonguFreni:
"""Ayni sohbet icin kisa surede cok sayida ozet istegini sayar."""
def __init__(self, sinir=3, pencere_sn=600):
self.sinir = sinir
self.pencere = pencere_sn
self.kayit = {}
def izin(self, anahtar, simdi):
"""Bu ozet istegine izin var mi? Varsa kaydeder."""
zamanlar = [t for t in self.kayit.get(anahtar, []) if simdi - t < self.pencere]
if len(zamanlar) >= self.sinir:
self.kayit[anahtar] = zamanlar
return False
zamanlar.append(simdi)
self.kayit[anahtar] = zamanlar
if len(self.kayit) > 500: # bellek sismesin
for k in sorted(self.kayit, key=lambda k: self.kayit[k][-1])[:250]:
self.kayit.pop(k, None)
return True
@@ -0,0 +1,94 @@
"""9Router'a giden isteklerde resim, video, ses, PDF ve dev içerikleri sadeleştirir (24 Eylül 2026, kullanıcı onayı).
Sorun: Codex, image_gen aracının ürettiği resmi (≈3 MB base64 PNG) araç çıktısı olarak sohbete geri koyuyor.
9Router'ın Claude çevirisi bunu düz yazı sayıyor: istek 3 milyon token'a çıktı ("prompt is too long:
3040251 tokens > 1000000 maximum") ve sohbetteki sonraki HER tur 400 ile düştü; sohbet kurtarılamadı.
Aynısı video, ses, PDF/dosya ve çok büyük araç çıktılarıyla da olabilir (kullanıcı isteği: "tekrar böyle sorun olmasın").
Kural:
- Medya parçaları (resim, dosya/PDF, ses, video): araç çıktılarında ve eski mesajlarda kısa notla değiştirilir.
Kullanıcının SON mesajındaki medya toplamı TUT_BAYT'ı aşmıyorsa korunur (yeni eklenen dosyayı model görebilsin).
- Tür ne olursa olsun tek başına DEV_BAYT'tan büyük her parça (son mesajdaki korunanlar hariç) notla değiştirilir.
- Düz yazı araç çıktısı DEV_BAYT'tan büyükse başı ve sonu kalır, ortası notla kısaltılır.
Codex'in kendi kaydına dokunulmaz; yalnız 9Router'a giden kopya değişir.
"""
import json
TUT_BAYT = 1_000_000
DEV_BAYT = 1_000_000
RESIM_TUT_BAYT = TUT_BAYT # eski ad
MEDYA_TURLERI = {'input_image': 'resim', 'image': 'resim', 'image_url': 'resim',
'input_file': 'dosya/PDF', 'file': 'dosya/PDF',
'input_audio': 'ses', 'audio': 'ses',
'input_video': 'video', 'video': 'video'}
def _boyut(p):
try:
return len(json.dumps(p, ensure_ascii=False))
except (TypeError, ValueError):
return len(str(p))
def _medya_mi(p):
return isinstance(p, dict) and p.get('type') in MEDYA_TURLERI
def _not(p):
tur = MEDYA_TURLERI.get(p.get('type'), p.get('type') or 'içerik') if isinstance(p, dict) else 'içerik'
ad = p.get('filename') or p.get('file_path') or p.get('path') or '' if isinstance(p, dict) else ''
ek = f' ({ad})' if ad else ''
return {'type': 'input_text',
'text': f'[{tur}{ek} çıkarıldı (AstraBridge): bu yoldan gönderilemiyor, ~{_boyut(p) // 1024} KB; '
'gerekirse dosyayı yolundan aç]'}
def _kisalt(metin):
yarim = DEV_BAYT // 4
return (metin[:yarim] + f'\n[... {len(metin) - 2 * yarim} karakter çıkarıldı (AstraBridge): çıktı çok büyüktü ...]\n'
+ metin[-yarim:])
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir."""
inp = req.get('input')
if not isinstance(inp, list):
return 0, 0
son_kullanici = None
for i, it in enumerate(inp):
if isinstance(it, dict) and it.get('type') == 'message' and it.get('role') == 'user':
son_kullanici = i
koru = set()
if son_kullanici is not None:
icerik = inp[son_kullanici].get('content')
if isinstance(icerik, list):
medya = [p for p in icerik if _medya_mi(p)]
if medya and sum(_boyut(p) for p in medya) <= tut_bayt:
koru.add(son_kullanici)
sayi = kazanc = 0
for i, it in enumerate(inp):
if not isinstance(it, dict) or i in koru:
continue
for alan in ('content', 'output'):
deger = it.get(alan)
if isinstance(deger, str) and len(deger) > DEV_BAYT:
yeni = _kisalt(deger)
kazanc += len(deger) - len(yeni)
sayi += 1
it[alan] = yeni
continue
if not isinstance(deger, list):
continue
degisti = False
yeni_liste = []
for p in deger:
if _medya_mi(p) or _boyut(p) > DEV_BAYT:
kazanc += _boyut(p)
sayi += 1
degisti = True
yeni_liste.append(_not(p))
else:
yeni_liste.append(p)
if degisti:
it[alan] = yeni_liste
return sayi, kazanc
@@ -0,0 +1,83 @@
"""Regression coverage for Codex -> external API access_programs compatibility."""
import ast
import copy
import json
from pathlib import Path
import unittest
from unittest.mock import Mock
import urllib.request
# Load only the actual adapter and routing method: no catalog/network/thread
# startup or credential loading is needed to exercise the production code.
source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
flatten = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "flatten_request")
claude_sonu = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt")
handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H")
responses = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_responses")
class AccessPrograms(unittest.TestCase):
def setUp(self):
self.upstream = Mock(return_value=Mock(status=200))
self.env = {
"json": json,
"urllib": Mock(request=Mock(Request=urllib.request.Request, urlopen=self.upstream)),
"decompress": lambda body, encoding: body,
"MODEL_MAP": {},
"CFG": {"nine_router_catalog": {"enabled": True, "route": "9router", "prefix": "9r/"},
"resim_sadelestir": False, "pano_kanca_user": False},
"ROUTES": {"9router": {"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": True}},
"fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(),
"log": lambda message: None,
"DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900,
}
exec(compile(ast.Module(body=[flatten, claude_sonu, responses], type_ignores=[]), "bridge.py", "exec"), self.env)
def request(self, model):
return {"model": model, "access_programs": {"cyber": "standard"},
"metadata": {"access_programs": "preserve nested data"},
"input": [{"role": "user", "content": [{"type": "input_text", "text": "hello"}]}],
"tools": [{"type": "function", "name": "echo", "parameters": {"type": "object", "properties": {}}}]}
def test_adapter_removes_only_top_level_field(self):
req = self.request("meta/muse-spark-1.3")
original = copy.deepcopy(req)
self.env["flatten_request"](req)
self.assertNotIn("access_programs", req)
self.assertEqual(req["metadata"], original["metadata"])
self.assertEqual(req["input"], original["input"])
self.assertEqual(req["tools"][0]["name"], "echo")
def test_absent_null_and_empty_values(self):
for value in (None, {}, {"cyber": "standard"}):
with self.subTest(value=value):
req = {"input": [], "access_programs": value}
self.env["flatten_request"](req)
self.assertNotIn("access_programs", req)
self.env["flatten_request"]({"input": []})
def test_model_and_combo_external_request_strips_field(self):
for model in ("9r/meta/muse-spark-1.3-contributor", "9r/Fusion-Hizli"):
with self.subTest(model=model):
body = json.dumps(self.request(model)).encode()
fake = Mock(headers={})
self.env["_responses"](fake, 1, "/responses", body)
sent = json.loads(self.upstream.call_args.args[0].data)
self.assertNotIn("access_programs", sent)
self.assertEqual(sent["model"], model.removeprefix("9r/"))
self.assertIn("access_programs", json.loads(body))
fake._stream.assert_called_once()
def test_chatgpt_passthrough_preserves_original_body(self):
body = json.dumps(self.request("gpt-6-astra")).encode()
fake = Mock(headers={})
self.env["_responses"](fake, 1, "/responses", body)
self.assertEqual(fake._passthrough.call_args.args[2], body)
self.upstream.assert_not_called()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,45 @@
"""claude_sonu_duzelt testleri (9Router Claude prefill 400)."""
import ast
import json
from pathlib import Path
import unittest
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
env = {}
exec(compile(ast.Module(body=[n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt"], type_ignores=[]), "bridge.py", "exec"), env)
f = env["claude_sonu_duzelt"]
def msg(role, text="x"):
return {"type": "message", "role": role, "content": [{"type": "input_text", "text": text}]}
class ClaudeSonu(unittest.TestCase):
def test_asistanla_biten_duzelir(self):
req = {"input": [msg("user"), msg("assistant")]}
self.assertTrue(f(req)[0])
self.assertEqual(req["input"][-1]["role"], "user")
def test_asistan_sonra_developer_duzelir(self):
# Sesli tur kapaninca: ... assistant, developer (9Router developer'i dusurur -> asistanla biter)
req = {"input": [msg("user"), msg("assistant"), msg("developer")]}
self.assertTrue(f(req)[0])
self.assertEqual(req["input"][-1]["role"], "user")
def test_reasoning_ile_biten_duzelir(self):
req = {"input": [msg("user"), {"type": "reasoning", "id": "rs_1"}]}
self.assertTrue(f(req)[0])
def test_kullaniciyla_biten_degismez(self):
req = {"input": [msg("assistant"), msg("user"), msg("developer")]}
self.assertFalse(f(req)[0])
self.assertEqual(len(req["input"]), 3)
def test_arac_sonucuyla_biten_degismez(self):
req = {"input": [msg("user"), {"type": "custom_tool_call", "call_id": "c"}, {"type": "custom_tool_call_output", "call_id": "c"}]}
self.assertFalse(f(req)[0])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,96 @@
"""A 9Router combo (Fusion-*) must not be sent twice once it may be running upstream.
22 Sep 2026: after a Cloudflare 524 the bridge resent 9r/Fusion-Orta three times and the whole
Fusion ran (and billed) again. Failures before the work starts may still be retried.
"""
import ast
import io
import json
from pathlib import Path
import types
import unittest
from unittest.mock import Mock
import urllib.error
import urllib.request
source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
pick = lambda name: next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == name)
handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H")
responses = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_responses")
def http_error(code, body=b"{}"):
return urllib.error.HTTPError("https://example.invalid", code, "x", {}, io.BytesIO(body))
class ComboNotResent(unittest.TestCase):
def setUp(self):
self.upstream = Mock()
self.env = {
"json": json, "re": __import__("re"), "os": __import__("os"),
"time": types.SimpleNamespace(sleep=lambda s: None),
"urllib": types.SimpleNamespace(
request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=self.upstream),
error=urllib.error),
"decompress": lambda body, encoding: body,
"MODEL_MAP": {},
"CFG": {"nine_router_catalog": {"enabled": True, "route": "9router", "prefix": "9r/"},
"resim_sadelestir": False, "pano_kanca_user": False},
"ROUTES": {"9router": {"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": False}},
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(), "log": lambda message: None,
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
"NO_RETRY_PATTERNS": [],
}
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses],
type_ignores=[]), "bridge.py", "exec"), self.env)
def send(self, model):
body = json.dumps({"model": model, "input": [{"role": "user", "content": [{"type": "input_text", "text": "8 vezir"}]}]}).encode()
fake = Mock(headers={})
self.env["_responses"](fake, 1, "/responses", body)
return fake
def test_combo_timeout_is_not_resent(self):
self.upstream.side_effect = urllib.error.URLError(TimeoutError("timed out"))
fake = self.send("9r/Fusion-Orta")
self.assertEqual(self.upstream.call_count, 1)
fake._stream.assert_not_called()
def test_combo_504_and_524_are_not_resent(self):
for code in (504, 524):
with self.subTest(code=code):
self.upstream.reset_mock()
self.upstream.side_effect = http_error(code)
self.send("9r/Fusion-Agir")
self.assertEqual(self.upstream.call_count, 1)
def test_combo_cut_mid_stream_is_not_resent(self):
self.upstream.side_effect = None
self.upstream.return_value = Mock(status=200)
fake = self.send("9r/Fusion-Orta")
fake._stream.side_effect = TimeoutError("read timed out")
self.upstream.reset_mock()
with self.assertRaises(TimeoutError):
self.send_with(fake, "9r/Fusion-Orta")
self.assertEqual(self.upstream.call_count, 1)
def send_with(self, fake, model):
body = json.dumps({"model": model, "input": [{"role": "user", "content": [{"type": "input_text", "text": "8 vezir"}]}]}).encode()
self.env["_responses"](fake, 1, "/responses", body)
def test_combo_failure_before_work_starts_is_retried(self):
self.upstream.side_effect = [http_error(503), urllib.error.URLError(ConnectionRefusedError("refused")), Mock(status=200)]
fake = self.send("9r/Fusion-Orta")
self.assertEqual(self.upstream.call_count, 3)
fake._stream.assert_called_once()
def test_single_model_timeout_is_still_retried(self):
self.upstream.side_effect = [urllib.error.URLError(TimeoutError("timed out")), Mock(status=200)]
fake = self.send("9r/cc/claude-opus-5-5")
self.assertEqual(self.upstream.call_count, 2)
fake._stream.assert_called_once()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,82 @@
import json, os, tempfile, unittest
import ide_catalog as c
def girdi(mid, ad, yetenek=("chat",), sira=0):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira}}
KATALOG = [
girdi("Ajan-Arac", "OmniRoute · Ajan-Arac", sira=5),
girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2),
girdi("9router-nano-x", "OmniRoute · nano/x", sira=1),
girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8),
girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"),
]
class Ayar(unittest.TestCase):
def test_dosya_yoksa_varsayilan_acik(self):
self.assertEqual(c.ayar_oku(os.path.join(tempfile.gettempdir(), "yok-boyle-dosya.json")), {"enabled": True, "kapsam": "ide"})
def test_kapatma_ve_bozuk_dosya(self):
with tempfile.TemporaryDirectory() as d:
yol = os.path.join(d, "a.json")
for icerik, beklenen in [('{"enabled": false}', {"enabled": False, "kapsam": "ide"}),
('{"enabled": true, "kapsam": "hepsi"}', {"enabled": True, "kapsam": "hepsi"}),
('{"enabled": "hayir", "kapsam": "x"}', {"enabled": True, "kapsam": "ide"}),
('bozuk', {"enabled": True, "kapsam": "ide"})]:
open(yol, "w", encoding="utf-8").write(icerik)
self.assertEqual(c.ayar_oku(yol), beklenen, icerik)
class Yapilandirma(unittest.TestCase):
def test_config_yaml_okuma(self):
with tempfile.TemporaryDirectory() as d:
open(os.path.join(d, "config.yaml"), "w", encoding="utf-8").write(
"model:\n default: Ajan-Arac\n provider: turkservis\nproviders:\n baska:\n models:\n Yabanci: {}\n"
" turkservis:\n base_url: https://ornek.test/v1/\n # yorum\n models:\n Hermes-Master: {}\n"
" 'cc/claude-opus-5-5': {}\n timeout: 3\nmcp_servers:\n x:\n models:\n Degil: {}\n")
open(os.path.join(d, ".env"), "w", encoding="utf-8").write("A=1\nHERMES_CUSTOM_TURKSERVIS_API_KEY='sahte-anahtar'\n")
self.assertEqual(c.ide_yapilandirma(d), ("https://ornek.test/v1", "Ajan-Arac", ["Hermes-Master", "cc/claude-opus-5-5"]))
self.assertEqual(c.anahtar_oku(d), "sahte-anahtar")
def test_dosyalar_yoksa(self):
with tempfile.TemporaryDirectory() as d:
self.assertEqual(c.ide_yapilandirma(d), (c.DEFAULT_BASE, "", []))
self.assertEqual(c.anahtar_oku(d), "")
class Secim(unittest.TestCase):
def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Tanimli model + adlandirilmis combolar; tanimsiz saglayici modeli ve gorsel modeli yok.
self.assertEqual([x["id"] for x in s], ["9router-cc-opus", "Ajan-Arac", "9router-Takim-Agir-6e65dba5", "9router-Takim-Agir"])
def test_gorunen_ad_yalniz_saglayici_ve_model(self):
for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"),
("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"),
("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"),
("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"),
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
self.assertEqual(c.kisa_ad(tam), beklenen, tam)
def test_cakisan_adlar_ayirt_edilir(self):
adlar = c.adlandir([("a", "nano/z-ai/glm-5.3"), ("b", "nano/baska/glm-5.3"), ("d", "cc/opus"), ("e-12345678", "OmniRoute · Takim-Agir")],
dolu={"takim-agir"})
self.assertEqual(adlar["a"], "nano/z-ai/glm-5.3") # kisa adi cakisan: aradaki halka geri gelir
self.assertEqual(adlar["b"], "nano/baska/glm-5.3")
self.assertEqual(adlar["d"], "cc/opus")
self.assertEqual(adlar["e-12345678"], "Takim-Agir [12345678]")
self.assertEqual(len(set(adlar.values())), 4)
def test_hepsi_kapsami_yalniz_sohbet(self):
s = c.sec(KATALOG, [], "hepsi")
self.assertEqual(len(s), 5)
self.assertNotIn("gorsel", [x["id"] for x in s])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,68 @@
"""Provider-switch history regression tests, without startup or network access."""
import ast
import copy
import json
from pathlib import Path
import unittest
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
env = {"json": json, "log": lambda message: None}
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES"} for t in node.targets):
nodes.insert(0, node)
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
class ModelSwitch(unittest.TestCase):
def test_external_to_chatgpt_preserves_messages_and_paired_tool_calls(self):
req = {"input": [
{"type": "reasoning", "id": "rs_native", "encrypted_content": "native-reasoning"},
{"type": "reasoning", "id": "rs_chatcmpl-foreign"},
{"type": "item_reference", "id": "chatcmpl-foreign"},
{"type": "message", "id": "msg_chatcmpl-foreign", "role": "assistant", "content": "remember-4827"},
{"type": "custom_tool_call", "id": "fc_chatcmpl-call", "call_id": "call_4827", "name": "probe", "input": "hello"},
{"type": "custom_tool_call_output", "id": "fc_chatcmpl-output", "call_id": "call_4827", "output": "result-4827"},
]}
self.assertEqual(env["fix_tool_ids"](req), 2)
self.assertEqual(env["drop_foreign_items"](req), 2)
self.assertEqual(req["input"][0]["encrypted_content"], "native-reasoning")
self.assertEqual(req["input"][1]["content"], "remember-4827")
self.assertEqual(req["input"][2]["id"], "ctc_chatcmpl-call")
self.assertEqual(req["input"][3]["id"], "ctco_chatcmpl-output")
self.assertEqual(req["input"][2]["call_id"], req["input"][3]["call_id"])
self.assertEqual(req["input"][3]["output"], "result-4827")
def test_chatgpt_to_external_preserves_custom_tool_history(self):
original = {"access_programs": {"cyber": "standard"}, "input": [
{"type": "custom_tool_call", "id": "ctc_4827", "call_id": "call_4827", "name": "probe", "input": "remember-4827"},
{"type": "custom_tool_call_output", "id": "ctco_4827", "call_id": "call_4827", "output": "result-4827"},
], "tools": [{"type": "namespace", "name": "functions", "tools": [{"type": "custom", "name": "probe"}]}]}
req = copy.deepcopy(original)
self.assertEqual(env["flatten_request"](req), {"probe"})
env["fix_tool_ids"](req)
self.assertEqual(json.loads(req["input"][0]["arguments"]), {"input": "remember-4827"})
self.assertEqual(req["input"][1]["output"], "result-4827")
self.assertEqual(req["input"][0]["call_id"], req["input"][1]["call_id"])
self.assertEqual(req["input"][0]["type"], "function_call")
self.assertEqual(req["input"][1]["type"], "function_call_output")
self.assertIn("access_programs", original)
def test_streamed_external_custom_tool_can_return_to_codex(self):
event = {"type": "response.output_item.done", "item": {
"type": "function_call", "id": "fc_chatcmpl-probe", "call_id": "call_4827",
"name": "probe", "arguments": json.dumps({"input": "remember-4827"})}}
raw = ("event: response.output_item.done\ndata: " + json.dumps(event) + "\n\n").encode()
adapter = env["SseRewriter"]({"probe"})
out = adapter.feed(raw[:25]) + adapter.feed(raw[25:]) + adapter.flush()
rewritten = json.loads(next(line[6:] for line in out.decode().splitlines() if line.startswith("data: ")))
self.assertEqual(rewritten["item"]["type"], "custom_tool_call")
self.assertEqual(rewritten["item"]["input"], "remember-4827")
self.assertEqual(rewritten["item"]["call_id"], "call_4827")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,145 @@
"""ozet_kopru testleri: 9Router icin sikistirma bagdastiricisi ve saglayicilar arasi ozet aktarimi."""
import json
import os
import tempfile
import unittest
import ozet_kopru as o
def msg(rol, metin):
return {"type": "message", "role": rol, "content": [{"type": "input_text", "text": metin}]}
def sse(*olaylar):
return b"".join(b"event: " + e["type"].encode() + b"\ndata: " + json.dumps(e, ensure_ascii=False).encode() + b"\n\n" for e in olaylar)
def coz(veri):
return [json.loads(s[5:]) for blok in veri.split(b"\n\n") for s in blok.split(b"\n") if s.startswith(b"data:")]
OPENAI_OZETI = {"type": "compaction", "id": "cmp_0123", "encrypted_content": "gAAAAABsahte", "internal_chat_message_metadata_passthrough": {"turn_id": "t"}}
class Istek(unittest.TestCase):
def test_tetik_algilanir_ve_ozet_istegine_cevrilir(self):
req = {"model": "m", "tools": [{"type": "function", "name": "x"}], "input": [msg("user", "KARAR-1: 14 Kasim"), {"type": "compaction_trigger"}]}
self.assertTrue(o.tetik_mi(req))
self.assertFalse(o.tetik_mi({"input": [msg("user", "normal")]}))
self.assertFalse(o.tetik_mi({"input": []}))
o.ozet_istegi_hazirla(req)
self.assertNotIn("compaction_trigger", json.dumps(req))
self.assertEqual(req["input"][0]["content"][0]["text"], "KARAR-1: 14 Kasim") # gecmis aynen kalir
self.assertEqual(req["input"][-1]["role"], "user")
istem = req["input"][-1]["content"][0]["text"]
for baslik in ("KARARLAR VE GEREKCELERI", "ACIK ISLER", "ONEMLI SONUCLAR", "KULLANICININ ISTEGI"):
self.assertIn(baslik, istem)
self.assertEqual(req["tool_choice"], "none")
def test_kopru_ozeti_her_hedefte_duz_metne_acilir(self):
oge = o.ozet_ogesi("KARAR-1: teslim 14 Kasim\nACIK-IS-1: fatura onayi")
self.assertTrue(oge["id"].startswith("cmp_astrabridge_"))
self.assertNotIn("KARAR-1", json.dumps(oge)) # ogede duz metin gorunmez (base64)
for yabanci in (False, True):
req = {"input": [msg("user", "once"), dict(oge), msg("user", "soru")]}
b = o.ozetleri_ac(req, yabanci_hedef=yabanci)
self.assertEqual(b["acilan"], 1)
metin = req["input"][1]["content"][0]["text"]
self.assertEqual(req["input"][1]["role"], "user")
self.assertIn("KARAR-1: teslim 14 Kasim", metin)
self.assertTrue(metin.startswith(o.OZET_BASLIK))
self.assertNotIn('"compaction"', json.dumps(req))
def test_openai_ozeti_openai_hedefinde_dokunulmaz(self):
req = {"input": [dict(OPENAI_OZETI), msg("user", "soru")]}
once = json.dumps(req, sort_keys=True)
b = o.ozetleri_ac(req, yabanci_hedef=False)
self.assertEqual(json.dumps(req, sort_keys=True), once)
self.assertEqual(b, {"acilan": 0, "cozulen": 0, "okunamayan": 0})
def test_openai_ozeti_yabanci_hedefte_duz_metne_cevrilir_ya_da_not_dusulur(self):
req = {"input": [dict(OPENAI_OZETI), msg("user", "soru")]}
b = o.ozetleri_ac(req, yabanci_hedef=True, coz=lambda oge: "KARAR-2: saglayici degismeyecek")
self.assertEqual(b["cozulen"], 1)
self.assertIn("KARAR-2", req["input"][0]["content"][0]["text"])
for bozuk in (None, lambda oge: None, lambda oge: 1 / 0):
req = {"input": [dict(OPENAI_OZETI), msg("user", "soru")]}
b = o.ozetleri_ac(req, yabanci_hedef=True, coz=bozuk)
self.assertEqual(b["okunamayan"], 1)
self.assertEqual(req["input"][0]["content"][0]["text"], o.OKUNAMADI_NOTU) # sessiz kayip yok
self.assertNotIn("gAAAAAB", json.dumps(req))
def test_coz_istegi_yalniz_gerekli_alanlari_tasir(self):
g = o.coz_istegi(OPENAI_OZETI, "gpt-6-luna")
self.assertEqual(g["input"][0], {"type": "compaction", "id": "cmp_0123", "encrypted_content": "gAAAAABsahte"})
self.assertEqual(g["model"], "gpt-6-luna")
self.assertFalse(g["store"])
class Akis(unittest.TestCase):
def _mesaj(self, metin, kimlik="msg_1"):
return {"id": kimlik, "type": "message", "role": "assistant", "content": [{"type": "output_text", "text": metin}]}
def test_metin_tek_compaction_ogesine_sarilir(self):
ozet = "1. KULLANICININ ISTEGI: x\n2. KARARLAR: KARAR-1 14 Kasim"
ham = sse({"type": "response.created", "response": {"id": "r1"}},
{"type": "response.output_item.added", "item": {"id": "msg_1", "type": "message"}},
{"type": "response.output_text.delta", "item_id": "msg_1", "delta": ozet[:10]},
{"type": "response.output_text.delta", "item_id": "msg_1", "delta": ozet[10:]},
{"type": "response.output_item.done", "item": self._mesaj(ozet)},
{"type": "response.completed", "response": {"id": "r1", "output": [self._mesaj(ozet)], "usage": {"input_tokens": 5}}})
a = o.OzetAkisi(tohum="7")
out = b"".join(a.feed(ham[i:i + 53]) for i in range(0, len(ham), 53)) + a.flush()
olaylar = coz(out)
self.assertEqual([e["type"] for e in olaylar], ["response.created", "response.output_item.done", "response.completed"])
oge = olaylar[1]["item"]
self.assertEqual(oge["type"], "compaction")
self.assertEqual(o.ozet_metni(oge), ozet)
self.assertEqual(olaylar[2]["response"]["output"], [oge]) # Codex tam bir compaction ogesi gorur
self.assertEqual(olaylar[2]["response"]["usage"], {"input_tokens": 5})
self.assertEqual(a.metin, ozet)
self.assertNotIn("KARAR-1", out.decode()) # ozet metni akista duz gorunmez
def test_yalniz_delta_gelirse_de_toplanir(self):
ham = sse({"type": "response.created", "response": {}},
{"type": "response.output_text.delta", "item_id": "a", "delta": "ozet "},
{"type": "response.output_text.delta", "item_id": "a", "delta": "metni"},
{"type": "response.completed", "response": {}})
a = o.OzetAkisi()
self.assertEqual(o.ozet_metni(coz(a.feed(ham))[1]["item"]), "ozet metni")
def test_metin_yoksa_compaction_uretilmez_hata_gorunur(self):
ham = sse({"type": "response.created", "response": {}},
{"type": "response.output_item.done", "item": {"type": "function_call", "name": "x"}},
{"type": "response.completed", "response": {"output": []}})
a = o.OzetAkisi()
olaylar = coz(a.feed(ham))
self.assertNotIn("compaction", json.dumps(olaylar))
self.assertFalse(a.metin)
hata = sse({"type": "response.created", "response": {}}, {"type": "response.failed", "response": {"error": {"message": "x"}}})
self.assertIn("response.failed", o.OzetAkisi().feed(hata).decode())
def test_onbellek(self):
klasor = os.path.join(tempfile.mkdtemp(), "c")
self.assertIsNone(o.onbellekten(klasor, OPENAI_OZETI))
o.onbellege(klasor, OPENAI_OZETI, "duz metin ozet")
self.assertEqual(o.onbellekten(klasor, OPENAI_OZETI), "duz metin ozet")
def test_sse_metni(self):
ham = sse({"type": "response.created", "response": {}}, {"type": "response.output_item.done", "item": self._mesaj("cozulmus ozet")},
{"type": "response.completed", "response": {}})
self.assertEqual(o.sse_metni(ham), "cozulmus ozet")
class Fren(unittest.TestCase):
def test_ayni_sohbette_ust_uste_ozet_durdurulur(self):
f = o.DonguFreni(sinir=3, pencere_sn=600)
self.assertEqual([f.izin("sohbet-a", t) for t in (0, 10, 20, 30, 40)], [True, True, True, False, False])
self.assertTrue(f.izin("sohbet-b", 40)) # baska sohbet etkilenmez
self.assertTrue(f.izin("sohbet-a", 700)) # pencere gecince yeniden izin
self.assertIn("Dongu durduruldu", o.DONGU_MESAJI % 3)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,71 @@
"""py -3 -m unittest test_resim_context (astra-bridge klasöründe)"""
import unittest
from resim_context import resimleri_sadelestir
def resim(bayt):
return {'type': 'input_image', 'image_url': 'data:image/png;base64,' + 'A' * bayt}
class Resim(unittest.TestCase):
def test_generated_image_in_tool_output_is_replaced(self):
req = {'input': [
{'type': 'message', 'role': 'user', 'content': [{'type': 'input_text', 'text': 'foto'}, resim(150_000)]},
{'type': 'custom_tool_call_output', 'call_id': 'x',
'output': [{'type': 'input_text', 'text': 'tamam'}, resim(3_000_000)]},
]}
sayi, kazanc = resimleri_sadelestir(req)
self.assertEqual(sayi, 1) # son user mesajındaki küçük foto korunur, araç çıktısındaki resim çıkar
self.assertGreater(kazanc, 3_000_000)
self.assertEqual(req['input'][1]['output'][1]['type'], 'input_text')
def test_latest_user_photos_kept_when_small(self):
req = {'input': [
{'type': 'message', 'role': 'user', 'content': [resim(10_000)]},
{'type': 'message', 'role': 'assistant', 'content': [{'type': 'output_text', 'text': 'ok'}]},
{'type': 'message', 'role': 'user', 'content': [{'type': 'input_text', 'text': 'yeni'}, resim(200_000)]},
]}
sayi, _ = resimleri_sadelestir(req)
self.assertEqual(sayi, 1) # yalnız eski mesajdaki resim
self.assertEqual(req['input'][2]['content'][1]['type'], 'input_image')
self.assertEqual(req['input'][0]['content'][0]['type'], 'input_text')
def test_latest_user_photos_dropped_when_too_big(self):
req = {'input': [{'type': 'message', 'role': 'user', 'content': [resim(1_200_000)]}]}
self.assertEqual(resimleri_sadelestir(req)[0], 1)
def test_pdf_audio_video_in_old_messages_and_tool_output_are_replaced(self):
pdf = {'type': 'input_file', 'filename': 'rapor.pdf', 'file_data': 'data:application/pdf;base64,' + 'P' * 500_000}
ses = {'type': 'input_audio', 'input_audio': {'data': 'S' * 400_000, 'format': 'wav'}}
video = {'type': 'input_video', 'video_url': 'data:video/mp4;base64,' + 'V' * 300_000}
req = {'input': [
{'type': 'message', 'role': 'user', 'content': [pdf, ses]},
{'type': 'function_call_output', 'call_id': 'y', 'output': [video]},
{'type': 'message', 'role': 'user', 'content': [{'type': 'input_text', 'text': 'devam'}]},
]}
sayi, _ = resimleri_sadelestir(req)
self.assertEqual(sayi, 3)
self.assertIn('rapor.pdf', req['input'][0]['content'][0]['text'])
self.assertIn('ses', req['input'][0]['content'][1]['text'])
self.assertIn('video', req['input'][1]['output'][0]['text'])
def test_huge_text_tool_output_is_shortened(self):
req = {'input': [{'type': 'function_call_output', 'call_id': 'z', 'output': 'x' * 3_000_000}]}
sayi, kazanc = resimleri_sadelestir(req)
self.assertEqual(sayi, 1)
self.assertLess(len(req['input'][0]['output']), 600_000)
self.assertIn('çıkarıldı', req['input'][0]['output'])
def test_unknown_huge_part_is_replaced(self):
req = {'input': [{'type': 'custom_tool_call_output', 'call_id': 'q',
'output': [{'type': 'yeni_bir_tur', 'veri': 'Z' * 1_500_000}]}]}
self.assertEqual(resimleri_sadelestir(req)[0], 1)
def test_request_without_images_untouched(self):
req = {'input': [{'type': 'message', 'role': 'user', 'content': [{'type': 'input_text', 'text': 'x'}]}]}
self.assertEqual(resimleri_sadelestir(req), (0, 0))
if __name__ == '__main__':
unittest.main()