Compare commits

...
Author SHA1 Message Date
yilsemandClaude Opus 5.5 959f5f1459 feat(codex-bridge): change the chat model by saying so; bridge 8, IDE 1.0.0-beta.27
A model picked on the phone only arrives with a typed message that starts a turn. The bridge now
handles "modeli X yap" itself: per-chat routing, same-turn handover when a model is working,
truthful answer to "which model", and a lone "." joining a running turn no longer cuts the work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:05:18 +03:00
yilsemandClaude Opus 5.5 acf88f8314 feat(codex-link): bridge 7 - external models work like subscription models
Measured with the unmodified Codex binary on 10 Oct 2026:
- real context windows per model (NanoGPT list, local table, combo = its
  smallest member, capped at the summarizer's window) so Codex compacts by
  itself when switching to a smaller model; an overflow guard trims
  oversized old parts and reports what still does not fit
- classic tools instead of code mode for external models: GLM 0/3 -> 3/3,
  Qwen 0/3 -> 2/3 on simple tool tasks, Claude/cx/Gemini unchanged
- phase (commentary / final_answer) on external assistant messages
- an answer cut by the client is carried to the next turn once
- images stay for vision-capable targets
- summary brake stops tight loops, not frequent compaction
- permanent gateway errors and tool-less targets fail at once with a
  readable message instead of five reconnects
- a lone "." (used to apply a model change in voice) is answered locally
- the bridge listens within seconds; the IDE waits 45 s for it

Release: AITURK IDE 1.0.0-beta.26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:04:58 +03:00
21 changed files with 1640 additions and 47 deletions

No files matched your search

@@ -26,3 +26,43 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
`POST /aiturk/codex-baglama/stop` ends the process.
Tests: `python -m unittest discover` in this directory.
## Version 8 (IDE beta.27): changing the model by saying so
Measured on a live voice chat (11 Oct 2026): a model picked on the phone reaches this machine only with a typed
message that STARTS a turn; a message that joins a running turn (including the lone `.`) does not carry it.
`model_gecis.py` therefore handles the change in the bridge. A short user message such as "modeli gemini flash low
yap" (spoken through the voice assistant or typed) is matched against the model list; the chat's requests are then
routed to that model (`model-yonlendirme.json`, keyed by `prompt_cache_key`) until Codex sends a different model
for the chat, i.e. until the user picks another one in the list.
- Idle chat: the bridge answers itself ("Sohbetin modeli artık X ..."), which also tells the voice assistant.
- Running turn: the command is replaced by a handover note and the SAME turn continues on the new model.
- "seçili model hangisi" / "ana model kim" is answered by the bridge with the model actually in use.
- An ambiguous name changes nothing and lists the closest names; a name matching no model is not a command.
- A lone `.` that joins a running turn is dropped instead of answered (answering it cut the work short).
- Handled commands left in the history are turned into notes so a later model does not answer them again.
Setting: `sesle_model` (default on).
## Version 7 (IDE beta.26): external models behave like subscription models
Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn).
| Area | Before | Now |
| --- | --- | --- |
| Context window | every added model declared 272 000 | `pencere.py`: NanoGPT's own list for `nano/*`, a local table for `cc`/`cx`/`ag`, a combo declares its smallest member, unknown = 128 000, nothing above 272 000 (the summarizer's window). Codex then compacts by itself when switching to a smaller model. |
| Overflow | the target answered with an empty stream; Codex retried five times | `pencere.sigdir` trims the middle of oversized old parts in the outgoing copy; if it still does not fit Codex gets `context_length_exceeded`. A limit reported by the target is learned (`pencere-ogrenilen.json`). |
| Tools | clones inherited `code_mode_only` + Responses Lite; GLM and Qwen failed 0/3 on simple tasks (empty answers, reasoning leaked into the answer) | `dis_arac_modu: "direct"` (default): classic tools. GLM 3/3, Qwen 2/3; Claude, `cx` and Gemini unchanged (6/6). `"kalip"` restores the old behaviour. |
| Intermediate messages | external assistant messages had no `phase`; Codex treats those as final answers | `SseRewriter` stamps `commentary` / `final_answer`. Deltas are never delayed; only the item's `done` event waits for what follows. |
| Interrupted answer | text written before a stop or an app exit was lost | `yarim.py` keeps it (memory only) when the *client* closes the stream and adds it as a note to the next user message of the same conversation, once. |
| Images | removed from every old message for all external models | kept for vision-capable targets (user messages, newest first, 1 MB total). |
| Summary brake | the 4th summary in 10 minutes failed the turn | three summaries in a row with no normal request in between, or eight in 10 minutes. |
| Gateway errors | `event: error` (e.g. `upstream 402`) looked like a cut stream | permanent ones become `response.failed` with a readable message; short upstream bodies are logged. |
| Start-up | the model list was fetched before listening; a slow gateway made the IDE give up after 10 s | window data comes from the cache at start and is refreshed in the background; the IDE waits 45 s. |
Not changed: subscription models, their order and names, and everything on the voice path (`/realtime/*` passes through untouched).
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> hazır." and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
+351 -20
View File
@@ -25,8 +25,11 @@ except ImportError:
from resim_context import resimleri_sadelestir
import ide_catalog
import ozet_kopru
import pencere
import yarim
import model_gecis
SURUM = "6"
SURUM = "8"
HERE = os.path.dirname(os.path.abspath(__file__))
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
@@ -165,13 +168,14 @@ load_9router_catalog()
IDE_PREFIX = "ide/"
IDE_ROUTE = "aiturk-ide"
IDE_DURUM = [None]
IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz)
def load_ide_catalog():
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)
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
@@ -195,7 +199,9 @@ def load_ide_catalog():
"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"]})
"kind": g["kind"], "context_window": g.get("pencere")})
IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"),
"kaynak": g.get("pencere_kaynak") or ""}
model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1
else:
@@ -207,11 +213,18 @@ def load_ide_catalog():
IDE_DURUM[0] = ozet
log(f"AITURK IDE Codex Baglama: {ozet}")
load_ide_catalog()
# 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):
@@ -410,12 +423,96 @@ def drop_foreign_items(req):
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."""
def __init__(self, custom):
"""function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE.
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None):
self.custom = custom
self.buf = b""
self.custom_ids = set()
self.bekleyen = None # phase karari bekleyen "output_item.done" olayi
self.arac_goruldu = False
self.hedef = hedef # gecit model kimligi (tasma siniri ogrenmek icin)
self.tasma = tasma # callable(hedef, hata metni): sinir ogrenildiyse cagrilir
self.aracsiz = aracsiz # callable(hedef): hedef "arac cagrisi desteklenmiyor" dedi
def _birak(self, phase):
d, self.bekleyen = self.bekleyen, None
if d is not None and not d["item"].get("phase"):
d["item"]["phase"] = phase
return d
def _sirala(self, d):
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
t = d.get("type", "")
if t == "error" and not isinstance(d.get("error"), dict) and d.get("message"):
# Gecidin duz bicimi: {"type":"error","code":"bad_request","message":"[400]: ...","status_code":400}
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
"status_code": d.get("status_code")}}
if t == "error" and isinstance(d.get("error"), dict):
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
metin = str(d["error"].get("message") or d["error"].get("type") or "")
if pencere.aracsiz_mi(metin):
if self.aracsiz is not None:
self.aracsiz(self.hedef)
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "invalid_request_error", "message":
"Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin."}}}]
if pencere.tasma_mi(metin):
if self.tasma is not None:
self.tasma(self.hedef, metin)
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key"
d = {"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}}
t = "response.failed"
item = d.get("item") if isinstance(d.get("item"), dict) else None
if item and item.get("type") in ARAC_OGELERI:
self.arac_goruldu = True
once = []
if self.bekleyen is not None and (t == "response.output_item.added"
or (t == "response.output_item.done" and item is not None)):
once.append(self._birak("commentary"))
if t in ("response.completed", "response.incomplete", "response.failed") and self.bekleyen is not None:
once.append(self._birak("commentary" if self.arac_goruldu else "final_answer"))
if t == "response.completed" and isinstance((d.get("response") or {}).get("output"), list):
phase_damgala(d["response"]["output"])
if t == "response.failed" and self.tasma is not None:
hata = (d.get("response") or {}).get("error") or {}
metin = str(hata.get("message") or "")
if pencere.tasma_mi(metin) or pencere.tasma_mi(str(hata.get("code") or "")):
hata["code"] = "context_length_exceeded" # Codex'in tanidigi tasma kodu
self.tasma(self.hedef, metin)
if t == "response.output_item.done" and asistan_mesaji_mi(item) and not item.get("phase"):
if self.arac_goruldu:
item["phase"] = "commentary"
else:
self.bekleyen = d
return once
return once + [d]
def _fix(self, d):
t = d.get("type", "")
item = d.get("item")
@@ -463,16 +560,68 @@ class SseRewriter:
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"
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.
@@ -508,6 +657,28 @@ def decompress(body, enc):
return body
OZET_FRENI = ozet_kopru.DonguFreni()
YONLENDIRME = model_gecis.Yonlendirme(HOME)
_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 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"
@@ -531,7 +702,7 @@ class H(http.server.BaseHTTPRequestHandler):
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):
def _stream(self, resp, status, tag, rewriter=None, izle=None):
self.send_response(status)
for k, v in resp.headers.items():
if k.lower() in RESP_HOP: continue
@@ -539,6 +710,7 @@ class H(http.server.BaseHTTPRequestHandler):
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
total = 0
bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir
def w(b):
if b:
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
@@ -547,12 +719,44 @@ class H(http.server.BaseHTTPRequestHandler):
chunk = resp.read(4096)
if not chunk: break
total += len(chunk)
if len(bas) < 600:
bas += chunk[:600 - len(bas)]
if izle is not None:
izle.feed(chunk)
w(rewriter.feed(chunk) if rewriter else chunk)
if rewriter: w(rewriter.flush())
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
log(f"{tag} istemci baglantiyi kapatti ({total} bayt)")
# Yanit bitmeden istemci kapatti (durdurma ya da uygulama kapanmasi): yarim metin sonraki tura tasinir.
if izle is not None and yarim.kaydet(izle):
log(f"{tag} yarim cevap saklandi ({len(izle.metin)} karakter); ayni sohbetin sonraki istegine not olarak eklenecek")
log(f"{tag} bitti status={status} {total} bayt")
if rewriter is not None and total < 600:
log(f"{tag} kisa akis govdesi: {bas[:500]!r}")
def _tasma_ogren(self, hedef, metin):
sinir = pencere.sinir_oku(metin)
if sinir and pencere.ogren(HOME, hedef, sinir):
log(f"pencere ogrenildi: {hedef} gercek siniri {sinir} token; model listesi yenileniyor")
threading.Thread(target=load_ide_catalog, daemon=True).start()
def _aracsiz_ogren(self, hedef):
if pencere.ogren_aracsiz(HOME, hedef):
log(f"aracsiz model ogrenildi: {hedef}; model listesinde isaretlenecek")
threading.Thread(target=load_ide_catalog, daemon=True).start()
def _send_sse_failed(self, kod, mesaj):
"""Codex'in akis icinde tanidigi hata bicimi (response.failed + error.code)."""
kimlik = "resp_astrabridge_%d" % int(time.time() * 1000)
olay = {"type": "response.failed", "response": {"id": kimlik, "object": "response", "status": "failed",
"error": {"code": kod, "message": mesaj}, "output": []}}
govde = b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n"
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
def _send_json(self, status, obj):
b = json.dumps(obj).encode()
@@ -605,7 +809,7 @@ class H(http.server.BaseHTTPRequestHandler):
return self._responses(n, path, body)
return self._passthrough(n, path, body)
def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None):
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:
@@ -658,7 +862,7 @@ class H(http.server.BaseHTTPRequestHandler):
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)
self._stream(resp, status, tag, rewriter, izle)
def _openai_ozet_coz(self, n, oge, ozet_cfg):
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
@@ -741,7 +945,18 @@ class H(http.server.BaseHTTPRequestHandler):
m["visibility"] = "list"
m["priority"] = x.get("priority", 0)
m["upgrade"] = None; m["availability_nux"] = None
m["additional_speed_tiers"] = []; m["service_tiers"] = []
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
@@ -795,6 +1010,54 @@ class H(http.server.BaseHTTPRequestHandler):
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 = YONLENDIRME.al(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)}."
sonuc = model_gecis.eslestir(komut["ad"], bilinen_modeller())
if not sonuc:
return None # ad hicbir modele benzemiyor: komut degil, modele gider
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 + "]")
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)))
return None
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.")
def _responses(self, n, path, body):
try:
req = json.loads(decompress(body, self.headers.get("Content-Encoding")))
@@ -802,7 +1065,56 @@ class H(http.server.BaseHTTPRequestHandler):
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)
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:
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)
try:
if model_gecis.gecmisi_temizle(req, bilinen_modeller()):
govde_degisti = True
except Exception as e:
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
hedef = YONLENDIRME.al(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"]):]
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)} hazır. Sohbetin modeli artık {model_adi(slug)}.")
# 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 {}
@@ -822,7 +1134,7 @@ class H(http.server.BaseHTTPRequestHandler):
# abonelik modeli: chatgpt.com'a oldugu gibi
fixed = fix_tool_ids(req)
atilan = drop_foreign_items(req)
degisti = False
degisti = bool(yarim_eklenen) or govde_degisti
ozet_akisi = None
if ozet_acik:
# Ozet bagdastirici (ozet_kopru.py; 9 Ekim 2026'da yalniz bu parca geri acildi): koprunun urettigi
@@ -832,7 +1144,7 @@ class H(http.server.BaseHTTPRequestHandler):
# 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))
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"[{n} CHATGPT {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
@@ -860,7 +1172,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} CHATGPT {slug}] ozet kopru: {ozet_akisi.sonuc_notu()}")
return sonuc
return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}",
rewritten=bool(fixed or atilan or degisti))
rewritten=bool(fixed or atilan or degisti), izle=izle)
route_name, target, flat = mapping
try:
ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req)
@@ -869,7 +1181,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}")
girdi_bayt = 0
if ozet_tetik:
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
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,
@@ -892,6 +1204,8 @@ class H(http.server.BaseHTTPRequestHandler):
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:
@@ -904,11 +1218,11 @@ class H(http.server.BaseHTTPRequestHandler):
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
rewriter = SseRewriter(custom)
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
fix_tool_ids(req)
if via_router and CFG.get('resim_sadelestir', True):
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
cikan, kazanc = resimleri_sadelestir(req)
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
if cikan:
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
@@ -926,6 +1240,18 @@ class H(http.server.BaseHTTPRequestHandler):
eklendi, sonlar = claude_sonu_duzelt(req)
if eklendi:
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
pencere_biliniyor = "varsayilan" not in ((IDE_BILGI.get(slug) or {}).get("kaynak") or "varsayilan")
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere)
if kirpilan:
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
if not sigdi:
log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})")
return self._send_sse_failed("context_length_exceeded",
f"Bu mesaj seçili modelin bağlam penceresine sığmıyor (yaklaşık {tahmin} token, pencere {hedef_pencere}). "
"Daha büyük pencereli bir model seçin ya da mesajı kısaltın.")
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt")
if DEBUG:
@@ -956,6 +1282,11 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
time.sleep(min(2 * attempt, 8)); continue
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
if pencere.tasma_mi(etxt):
# Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin
# bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur).
self._tasma_ogren(target, etxt)
return self._send_sse_failed("context_length_exceeded", etxt[:300])
self.send_response(e.code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(errbody)))
@@ -973,7 +1304,7 @@ class H(http.server.BaseHTTPRequestHandler):
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
return self._stream(resp, resp.status, tag, rewriter)
return self._stream(resp, resp.status, tag, rewriter, izle)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
do_POST = _handle
@@ -11,6 +11,8 @@ Yerel config'teki model adlari uye katalogunu sinirlamaz.
"""
import json, os, re, urllib.request
import pencere
# 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))
@@ -172,7 +174,8 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
if kind == "combo" and mid.lower().startswith("9router-"):
continue
gorulen.add(mid)
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0})
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0,
"yol": str(m.get("description") or "").split(" · ")[-1].strip()})
secilen.sort(key=lambda s: (s["sira"], s["id"]))
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
@@ -181,7 +184,7 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
adlar.add(taban.split(" · ")[-1].strip().lower())
s["ad"] = taban
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"]} for s in secilen]
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen]
def sirala_codex(modeller, ekler):
@@ -230,6 +233,7 @@ def sirala_codex(modeller, ekler):
# Telefonda bu satir tek satirdir ve yaklasik 30 karakterde kesilir (kullanici ekran goruntusu,
# 10 Ekim 2026); en ayirt edici bilgi basa yazilir, ad tekrar edilmez.
ACIKLAMA_SINIRI = 30
KUCUK_PENCERE = 100_000 # bunun altindaki pencere alt satirda yazilir
_AILE = (("opus", "opus"), ("sonnet", "sonnet"), ("haiku", "haiku"), ("astra", "astra"), ("terra", "terra"),
("luna", "luna"), ("sol", "sol"), ("gemini", "gemini"), ("qwen", "qwen"), ("glm", "glm"),
("abliterated", "abliterated"), ("deepseek", "deepseek"), ("llama", "llama"), ("gemma", "gemma"))
@@ -273,9 +277,11 @@ def _sigdir(onek, adlar, ayrac):
return onek.rstrip(": ")
def aciklama(kimlik, ad, tur, kayit=None):
def aciklama(kimlik, ad, tur, kayit=None, bilgi=None):
"""Model adinin altinda gorunecek kisa satir. kayit: gecidin /router/catalog girdisi (yoksa None).
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri."""
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri.
bilgi (pencere.coz sonucu): Codex'te isi etkileyen iki uyari one yazilir: kucuk baglam ("32K")
ve arac cagiramayan model ("araçsız")."""
if tur == "combo":
topoloji = (kayit or {}).get("topology") or {}
uyeler = topoloji.get("members") or []
@@ -292,18 +298,25 @@ def aciklama(kimlik, ad, tur, kayit=None):
kisa = gorunen_ad(kimlik, ad, "model")
kod = kisa.split("/")[0].lower() if "/" in kisa else (kimlik or "").split("-")[0].lower()
parcalar = [_SAGLAYICI.get(kod, kod) or "Tek model"]
if bilgi and bilgi.get("pencere") and bilgi["pencere"] < KUCUK_PENCERE:
parcalar.append(pencere.kisa(bilgi["pencere"]))
if bilgi and bilgi.get("arac") is False:
parcalar.append("araçsız")
if _SANSURSUZ.search(kisa) or _SANSURSUZ.search(kimlik or ""):
parcalar.append("sansürsüz")
if "thinking" in (kisa + " " + (kimlik or "")).lower():
parcalar.append("düşünen")
if len(parcalar) == 1:
parcalar.insert(0, "Tek model")
while len(parcalar) > 1 and len(" · ".join(parcalar)) > ACIKLAMA_SINIRI:
parcalar.pop() # sona yazilan en az onemli ozellik duser; kelime ortasindan kesilmez
return " · ".join(parcalar)[:ACIKLAMA_SINIRI]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
Ag hatasinda istisna atar; cagiran son listeyi korur."""
Ag hatasinda istisna atar; cagiran son listeyi korur. home: koprunun veri klasoru (pencere onbellegi).
Her girdi "pencere" (ilan edilecek baglam penceresi), "gorsel" ve "arac" alanlarini tasir."""
ayar = ayar_oku()
if not ayar["enabled"]:
return "kapali", "", "", []
@@ -328,6 +341,12 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
ogr = pencere.ogrenilen(home)
aracsiz = pencere.aracsizlar(home)
for g in girdiler:
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]))
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
bilgi["arac"], bilgi["kaynak"])
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]), bilgi)
return "tamam", base, anahtar, girdiler
@@ -0,0 +1,259 @@
"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026).
Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla
ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu.
Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik
koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir),
kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir.
- Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ...").
- Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer;
yarim kalan is kesilmez, yeni model kaldigi yerden devam eder.
- Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar.
Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur.
"""
import json
import os
import re
import threading
import time
DOSYA = "model-yonlendirme.json"
OMUR_SN = 7 * 24 * 3600
EN_COK_KAYIT = 200
EN_COK_KELIME = 14
_KILIT = threading.Lock()
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
SORU = ("hangi", "hangisi", "kim", "kimdir")
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
"bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex",
"ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona"}
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
"kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex",
"dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku",
"sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking",
"ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"}
# Listede kisa yazilan saglayici -> konusurken soylenen ad
SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")}
def duz(metin):
return str(metin or "").lower().translate(_TR)
def parcala(metin):
"""Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low."""
return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))]
def son_sira(girdi):
"""Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar
kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1."""
if not isinstance(girdi, list):
return -1
for i in range(len(girdi) - 1, -1, -1):
oge = girdi[i]
if isinstance(oge, dict) and oge.get("type", "message") == "message":
if oge.get("role") == "user":
return i
if oge.get("role") in ("developer", "system"):
continue
return -1
return -1
def son_kullanici_metni(req):
"""(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False).
Sesli aktarimda (<realtime_delegation>) yalnizca <input> icindeki soz alinir."""
girdi = req.get("input")
sira = son_sira(girdi)
if sira < 0:
return None, -1, False
son = girdi[sira]
icerik = son.get("content")
if isinstance(icerik, str):
metin = icerik
elif isinstance(icerik, list) and icerik 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 None, -1, False
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
if m:
return m.group(1).strip(), sira, True
if "<" in metin and ">" in metin:
return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz
return metin.strip(), sira, False
def komut_coz(metin):
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
kelimeler = re.findall(r"[a-z0-9]+", duz(metin))
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
return None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"}
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
if fiil:
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
return {"tur": "gec", "ad": ad} if ad else None
return None
def _uyar(sorgu, parca):
"""Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar."""
if sorgu == parca:
return True
return len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit()
def eslestir(ad, modeller):
"""ad: sorgu parcalari; modeller: [(slug, gorunen ad)].
Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor)."""
if not ad:
return None
puanli = []
for slug, gorunen in modeller:
parcalar = parcala(gorunen) or parcala(slug)
kume = set(parcalar) | set(parcala(slug))
for kisa, uzunlar in SAGLAYICI.items():
if kisa in kume:
kume.update(uzunlar)
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
puanli.append((tutan, -len(parcalar), slug, gorunen))
if not puanli:
return None
en = max(p[0] for p in puanli)
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
return None # sozun cogu hicbir modele uymuyor: komut degil
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
return {"slug": iyi[0][2], "ad": iyi[0][3]}
return {"adaylar": [p[3] for p in iyi[:4]]}
_ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning",
"local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"}
def tur_ortasi(req, sira):
"""Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir:
once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir."""
girdi = req.get("input") or []
for oge in reversed(girdi[:max(sira, 0)]):
if not isinstance(oge, dict):
continue
tur = oge.get("type", "message")
if tur in _ARAC:
return True
if tur == "message":
if oge.get("role") == "assistant":
return oge.get("phase") == "commentary"
continue
return False
def metni_degistir(req, sira, yeni):
"""sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca <input> icini)."""
oge = req["input"][sira]
icerik = oge.get("content")
eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik)
if "<realtime_delegation>" in eski:
metin = re.sub(r"(<input>).*?(</input>)", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S)
else:
metin = yeni
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
def gecmisi_temizle(req, modeller):
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026).
Doner: degistirilen mesaj sayisi."""
girdi = req.get("input")
if not isinstance(girdi, list):
return 0
son = son_sira(girdi)
n = 0
for i, oge in enumerate(girdi):
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
continue
metin, _, _ = son_kullanici_metni({"input": [oge]})
if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"):
continue
komut = komut_coz(metin)
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
if sonuc and "slug" in sonuc:
metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; "
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
n += 1
return n
def devir_notu(yeni_ad, eski_ad):
return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren "
f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]")
class Yonlendirme:
"""sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir."""
def __init__(self, home):
self.yol = os.path.join(home, DOSYA) if home else None
self.veri = {}
if self.yol:
try:
with open(self.yol, encoding="utf-8") as f:
okunan = json.load(f)
if isinstance(okunan, dict):
self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")}
except (OSError, ValueError):
pass
def _yaz(self):
if not self.yol:
return
try:
gecici = self.yol + ".tmp"
with open(gecici, "w", encoding="utf-8") as f:
json.dump(self.veri, f, ensure_ascii=False)
os.replace(gecici, self.yol)
except OSError:
pass
def al(self, sohbet, gelen, simdi=None):
"""Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni
model secti) ya da kayit eskidiyse yonlendirme silinir."""
simdi = simdi or time.time()
with _KILIT:
k = self.veri.get(sohbet)
if not k:
return None
if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN:
del self.veri[sohbet]
self._yaz()
return None
return k["hedef"]
def koy(self, sohbet, gelen, hedef, simdi=None):
simdi = simdi or time.time()
with _KILIT:
if hedef == gelen:
self.veri.pop(sohbet, None)
else:
self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi}
if len(self.veri) > EN_COK_KAYIT:
for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]:
del self.veri[eski]
self._yaz()
+14 -3
View File
@@ -281,22 +281,33 @@ DONGU_MESAJI = ("AstraBridge: Bu sohbetin baglami, ozetlendikten sonra da secili
class DonguFreni:
"""Ayni sohbet icin kisa surede cok sayida ozet istegini sayar."""
"""Ozet dongusunu durdurur, mesru sik sikistirmayi durdurmaz (10 Ekim 2026: kucuk esikli sohbette
10 dakikadaki 4. ozet reddedilip tur dusuyordu). Iki ayri sinir:
- art_arda: arada normal (ozet olmayan) istek gecmeden ust uste gelen ozet sayisi; gercek dongu budur.
- sinir: pencere_sn icindeki toplam ozet sayisi; her ozetin arasina normal istek giren yavas donguyu keser."""
def __init__(self, sinir=3, pencere_sn=600):
def __init__(self, sinir=8, pencere_sn=600, art_arda=3):
self.sinir = sinir
self.pencere = pencere_sn
self.art_arda = art_arda
self.kayit = {}
self.ust_uste = {}
def normal(self, anahtar):
"""Bu sohbette ozet olmayan bir istek gecti: art arda sayaci sifirlanir."""
self.ust_uste.pop(anahtar, None)
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:
if len(zamanlar) >= self.sinir or self.ust_uste.get(anahtar, 0) >= self.art_arda:
self.kayit[anahtar] = zamanlar
return False
zamanlar.append(simdi)
self.kayit[anahtar] = zamanlar
self.ust_uste[anahtar] = self.ust_uste.get(anahtar, 0) + 1
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)
self.ust_uste.pop(k, None)
return True
+292
View File
@@ -0,0 +1,292 @@
"""Eklenen modellerin gercek baglam penceresi ve yetenekleri (10 Ekim 2026).
Neden: gecit model basina pencere vermiyor; kopru her modele abonelik kalibinin penceresini (272000)
kopyaliyordu. Codex, kucuk pencereli modele geciste sohbeti kendisi sikistirir, ama bunu ilan edilen
pencereye gore yapar (core/src/session/turn.rs maybe_run_previous_model_inline_compact). Pencere dogru
ilan edilince o yol kendiliginden calisir; yanlis ilan edilince istek hedefe sigmaz.
Kaynak sirasi (kucukten guvenilire degil, guvenilirden yedege):
1. Hedefin kendi hatasindan ogrenilen sinir (ogren / ogrenilen): en guvenilir, diske yazilir.
2. NanoGPT'nin acik model listesi (nano/... modelleri icin istegi tasiyan saglayicinin kendi degeri).
3. Saglayici kisaltmasina gore yerel tablo (cc, cx, ag).
4. Bilinmeyen: VARSAYILAN (temkinli) ve bir kez gunluk satiri.
Combo: yapraklarinin en kucugu (yedege dusulurse de sigsin).
Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asmamali.
"""
import json
import os
import re
import time
import urllib.request
TAVAN = 272_000 # abonelik modellerinin penceresi; ozetleyen model bundan fazlasini okuyamaz
VARSAYILAN = 128_000 # hakkinda hicbir sey bilinmeyen model
TABAN = 8_000 # saglayici verisi bozuksa (0, eksi) kullanilmaz
NANO_URL = "https://nano-gpt.com/api/v1/models?detailed=true"
NANO_OMUR_SN = 24 * 3600
NANO_DOSYA = "pencere-nano.json"
OGRENILEN_DOSYA = "pencere-ogrenilen.json"
ARACSIZ_DOSYA = "aracsiz-modeller.json"
# Saglayici kisaltmasi -> (pencere, gorsel girdi, arac cagrisi). cc = Claude Code aboneligi, cx = Codex
# aboneligi, ag = Antigravity. Degerler abonelik yollarinin temkinli sinirlaridir.
_YEREL = (
(re.compile(r"^cx/"), (272_000, True, True)),
(re.compile(r"^cc/"), (200_000, True, True)),
(re.compile(r"^ag/claude"), (200_000, True, True)),
(re.compile(r"^ag/gemini"), (1_000_000, True, True)),
(re.compile(r"^ag/gpt-oss"), (131_072, False, True)),
)
# "maximum context length is 131072 tokens", "prompt is too long: 3040251 tokens > 1000000 maximum",
# "context window of 32768 tokens", "exceeds the limit of 65536" ...
_SINIR = (
re.compile(r"maximum context length (?:is|of) (\d{4,8})", re.I),
re.compile(r"\d{4,9} tokens? > (\d{4,8}) maximum", re.I),
re.compile(r"context (?:window|length|size)(?: limit)? (?:is|of) (\d{4,8})", re.I),
re.compile(r"(?:limit|maximum) of (\d{4,8}) tokens", re.I),
re.compile(r"max(?:imum)? context tokens\D{0,6}(\d{4,8})", re.I),
)
_TASMA = re.compile(r"context[_ ](?:length|window)[_ ]exceeded|maximum context length|prompt is too long|"
r"context window|too many tokens|input is too long|exceeds? the (?:maximum |model.s )?context|max(?:imum)? context tokens", re.I)
def _oku(yol):
try:
with open(yol, encoding="utf-8") as f:
v = json.load(f)
return v if isinstance(v, dict) else {}
except (OSError, ValueError):
return {}
def _yaz(yol, veri):
gecici = yol + ".tmp"
with open(gecici, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol)
def nano_sadelestir(ham):
"""NanoGPT yaniti -> {model kimligi (kucuk harf): {"pencere", "gorsel", "arac"}}."""
cikti = {}
for m in (ham or {}).get("data") or []:
kimlik = str(m.get("id") or "").lower()
pencere = m.get("context_length")
if not kimlik or not isinstance(pencere, int) or pencere < TABAN:
continue
yetenek = m.get("capabilities") or {}
cikti[kimlik] = {"pencere": pencere, "gorsel": bool(yetenek.get("vision")),
"arac": bool(yetenek.get("tool_calling"))}
return cikti
def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir=None, ag=True):
"""Onbellek tazeyse onu, degilse agdan yenisini dondurur. Ag hatasinda eski onbellek (yasina bakilmadan)
kullanilir; o da yoksa bos sozluk doner ve cagiran yerel tabloya duser. Hicbir durumda istisna atmaz.
ag=False: aga cikilmaz, eldeki onbellek doner (koprunun acilisi beklemesin diye)."""
simdi = time.time() if simdi is None else simdi
yol = os.path.join(home, NANO_DOSYA) if home else ""
onbellek = _oku(yol) if yol else {}
if onbellek.get("modeller") and simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN:
return onbellek["modeller"]
if not ag:
return onbellek.get("modeller") or {}
try:
if getir is None:
istek = urllib.request.Request(NANO_URL, headers={"User-Agent": ua, "Accept": "application/json"})
with urllib.request.urlopen(istek, timeout=timeout) as resp:
ham = json.load(resp)
else:
ham = getir()
modeller = nano_sadelestir(ham)
if modeller:
if yol:
try:
_yaz(yol, {"zaman": simdi, "modeller": modeller})
except OSError:
pass
return modeller
except Exception: # noqa: BLE001 - ag, JSON, sertifika: hepsi "yerel tabloya dus" demek
pass
return onbellek.get("modeller") or {}
def ogrenilen(home):
return {k: int(v) for k, v in _oku(os.path.join(home, OGRENILEN_DOSYA)).items()
if isinstance(v, (int, float)) and v >= TABAN} if home else {}
def aracsizlar(home):
"""Hedefin "arac cagrisi desteklenmiyor" dedigi modeller (kimlik kumesi)."""
return {k for k, v in _oku(os.path.join(home, ARACSIZ_DOSYA)).items() if v} if home else set()
def aracsiz_mi(metin):
return bool(re.search(r"does not support tool|tool_choice_unsupported|tools? (?:are|is) not supported|"
r"function calling is not supported", metin or "", re.I))
def ogren_aracsiz(home, kimlik):
"""True: kayit yeni eklendi."""
if not home or not kimlik:
return False
yol = os.path.join(home, ARACSIZ_DOSYA)
veri = _oku(yol)
if veri.get(kimlik):
return False
veri[kimlik] = True
try:
_yaz(yol, veri)
except OSError:
return False
return True
def tasma_mi(metin):
return bool(_TASMA.search(metin or ""))
def sinir_oku(metin):
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
for kalip in _SINIR:
m = kalip.search(metin or "")
if m and int(m.group(1)) >= TABAN:
return int(m.group(1))
return None
def ogren(home, kimlik, sinir):
"""Hedefin bildirdigi siniri kaydeder (yalniz kucultur). True: kayit degisti."""
if not home or not kimlik or not sinir or sinir < TABAN:
return False
yol = os.path.join(home, OGRENILEN_DOSYA)
veri = _oku(yol)
if isinstance(veri.get(kimlik), (int, float)) and veri[kimlik] <= sinir:
return False
veri[kimlik] = int(sinir)
try:
_yaz(yol, veri)
except OSError:
return False
return True
def yol_bilgisi(yol, nano):
""""nano/z-ai/glm-5.3" ya da "cc/claude-opus-5-5" -> {"pencere","gorsel","arac","kaynak"} ya da None."""
yol = (yol or "").strip()
if yol.startswith("9router/"):
yol = yol[len("9router/"):]
if yol.startswith("nano/"):
n = (nano or {}).get(yol[len("nano/"):].lower())
if n:
return {**n, "kaynak": "nanogpt"}
return None
for kalip, (pencere, gorsel, arac) in _YEREL:
if kalip.match(yol):
return {"pencere": pencere, "gorsel": gorsel, "arac": arac, "kaynak": "yerel"}
return None
def _yapraklar(dugum, derinlik=0):
if not isinstance(dugum, dict) or derinlik > 6:
return []
if dugum.get("kind") != "combo":
return [dugum.get("model")] if dugum.get("model") else []
cikti = []
for uye in dugum.get("members") or []:
cikti.extend(_yapraklar(uye, derinlik + 1))
return cikti
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
bilinmeyen = {"pencere": VARSAYILAN, "gorsel": False, "arac": True, "kaynak": "varsayilan"}
if tur == "combo":
yapraklar = _yapraklar((kayit or {}).get("topology"))
bilgiler = [yol_bilgisi(y, nano) or dict(bilinmeyen) for y in yapraklar]
if not bilgiler:
sonuc = dict(bilinmeyen)
else:
sonuc = {"pencere": min(b["pencere"] for b in bilgiler),
# Bir takim uyesi resmi goremese de digerleri gorur; sirali yedekte ilk uye belirler.
"gorsel": bool(bilgiler[0]["gorsel"]), "arac": bool(bilgiler[0]["arac"]),
"kaynak": "uyeler:" + ",".join(sorted({b["kaynak"] for b in bilgiler}))}
else:
if (yol or "").startswith("no-think/"):
# Dusunmesiz kopya: asil saglayici kimlikten okunur ("no-think-9router-cc-claude-..." -> cc).
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
if kimlik in aracsiz:
sonuc = {**sonuc, "arac": False}
sinir = (ogr or {}).get(kimlik)
if sinir and sinir < sonuc["pencere"]:
sonuc = {**sonuc, "pencere": int(sinir), "kaynak": "ogrenilen"}
sonuc["pencere"] = max(TABAN, min(TAVAN, int(sonuc["pencere"])))
return sonuc
def kisa(pencere):
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
# ---- Istek oncesi tasma bekcisi ----
# Codex sikistirmadan sonra kullanici mesajlarini AYNEN tutar; cok buyuk bir yapistirma (ya da dev arac
# ciktisi) kucuk pencereli hedefe yine sigmaz ve hedef bos akisla doner (10 Ekim 2026 olcumu: 390 KB istek,
# 64K pencere, Codex 5 kez yeniden deneyip "stream closed" ile dustu). Bekci, hedefe giden KOPYADA en buyuk
# eski metin parcalarinin ortasini cikarir; yine sigmazsa istegi gondermeden anlasilir hata doner.
BAYT_BOLU_TOKEN = 3.2 # olcum (10 Ekim 2026): 540 KB duz Turkce metin = 171016 token (3,16 bayt/token)
DOLULUK = 0.97 # pencerenin bu oranini asan istek kirpilir / gonderilmez
KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
KIRP_BAS = KIRP_SON = 1500
def tahmini_token(req):
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / BAYT_BOLU_TOKEN)
def _metin_parcalari(req):
"""Kirpilabilir (sahip, anahtar) ciftleri: arac ciktilari ile eski kullanici/asistan mesajlari.
Son kullanici mesajina ve talimat tasiyan developer/system mesajlarina (Codex kurallari, skill
yonergeleri, kanca baglami) dokunulmaz."""
girdi = req.get("input")
if not isinstance(girdi, list):
return []
son_kullanici = max((i for i, it in enumerate(girdi) if isinstance(it, dict) and it.get("role") == "user"
and it.get("type", "message") == "message"), default=-1)
parcalar = []
for i, it in enumerate(girdi):
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
continue
for alan in ("content", "output"):
deger = it.get(alan)
if isinstance(deger, str):
parcalar.append((it, alan))
elif isinstance(deger, list):
for p in deger:
if isinstance(p, dict) and isinstance(p.get("text"), str):
parcalar.append((p, "text"))
return parcalar
def sigdir(req, pencere_token):
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
sinir = int(pencere_token * DOLULUK)
tahmin = tahmini_token(req)
if not pencere_token or tahmin <= sinir:
return 0, True, tahmin
kirpilan = 0
parcalar = sorted(_metin_parcalari(req), key=lambda sa: len(sa[0][sa[1]]), reverse=True)
for sahip, anahtar in parcalar:
metin = sahip[anahtar]
if len(metin) < KIRP_ESIK:
break
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
kirpilan += 1
tahmin = tahmini_token(req)
if tahmin <= sinir:
return kirpilan, True, tahmin
return kirpilan, tahmin <= sinir, tahmin
@@ -49,8 +49,11 @@ def _kisalt(metin):
+ 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."""
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir.
gorsel=True (hedef model resim görebiliyor): eski KULLANICI mesajlarındaki resimler de, en yeniden eskiye
toplam tut_bayt'ı aşmadıkça korunur (10 Ekim 2026: resim görebilen modele geçince "göremiyorum" diyordu).
Araç çıktılarındaki medya her durumda notla değiştirilir."""
inp = req.get('input')
if not isinstance(inp, list):
return 0, 0
@@ -65,6 +68,20 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
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)
if gorsel:
kalan = tut_bayt - sum(_boyut(p) for i in koru for p in inp[i].get('content') or [] if _medya_mi(p))
for i in range(len(inp) - 1, -1, -1):
it = inp[i]
if i in koru or not (isinstance(it, dict) and it.get('type') == 'message' and it.get('role') == 'user'):
continue
icerik = it.get('content')
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
if isinstance(icerik, list) else []
diger = [p for p in icerik if _medya_mi(p) and p not in resimler] if isinstance(icerik, list) else []
boyut = sum(_boyut(p) for p in resimler)
if resimler and not diger and boyut <= kalan:
koru.add(i)
kalan -= boyut
sayi = kazanc = 0
for i, it in enumerate(inp):
if not isinstance(it, dict) or i in koru:
@@ -31,7 +31,7 @@ class AccessPrograms(unittest.TestCase):
"fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(),
"log": lambda message: None,
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"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)
@@ -40,7 +40,7 @@ class ComboNotResent(unittest.TestCase):
"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": [],
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
}
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses],
type_ignores=[]), "bridge.py", "exec"), self.env)
@@ -0,0 +1,91 @@
"""Bridge 7: tool-less models are learned, flat gateway errors are translated, instructions are never trimmed."""
import ast
import json
import tempfile
import unittest
from pathlib import Path
import pencere
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
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", "ARAC_OGELERI", "NOKTA_ISARETLERI"} for t in node.targets):
nodes.insert(0, node)
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
def feed(event, **kw):
adapter = env["SseRewriter"](set(), **kw)
raw = b"data: " + json.dumps(event).encode() + b"\n\n"
return [json.loads(line[6:]) for line in (adapter.feed(raw) + adapter.flush()).decode().splitlines()
if line.startswith("data: ")]
class Ek(unittest.TestCase):
def test_model_without_tool_calls_is_reported_and_remembered(self):
seen = []
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
out = feed(flat, hedef="m", aracsiz=seen.append)
self.assertEqual(out[0]["type"], "response.failed")
self.assertIn("araç çağrısını desteklemiyor", out[0]["response"]["error"]["message"])
self.assertEqual(seen, ["m"])
home = tempfile.mkdtemp()
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
self.assertFalse(pencere.ogren_aracsiz(home, "m"))
self.assertFalse(pencere.coz("m", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
self.assertTrue(pencere.coz("n", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
def test_flat_overflow_error_is_translated_and_learned(self):
seen = []
flat = {"type": "error", "code": "bad_request", "message":
"Your prompt exceeds the model's context length. Max context tokens: 65536."}
out = feed(flat, hedef="m", tasma=lambda hedef, metin: seen.append(pencere.sinir_oku(metin)))
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
self.assertEqual(seen, [65536])
self.assertEqual(feed({"type": "error", "code": "server_error", "message": "upstream 503"})[0]["type"], "error")
def test_guard_never_trims_instruction_messages(self):
big = "KURAL " * 40000
req = {"input": [
{"type": "message", "role": "developer", "content": [{"type": "input_text", "text": big}]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "eski " * 40000}]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "soru"}]}]}
pencere.sigdir(req, 65536)
self.assertEqual(req["input"][0]["content"][0]["text"], big)
self.assertIn("çıkarıldı (köprü)", req["input"][1]["content"][0]["text"])
class Nokta(unittest.TestCase):
def user(self, content):
return {"type": "message", "role": "user", "content": content}
def test_only_a_lone_dot_from_the_user_is_answered_locally(self):
is_dot = env["nokta_mesaji_mi"]
self.assertTrue(is_dot({"input": [self.user([{"type": "input_text", "text": " . "}])]}))
self.assertTrue(is_dot({"input": [self.user("x"), self.user(".")]}))
for other in ("..", "?", "Selam", "", "1."):
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": other}])]}), other)
self.assertFalse(is_dot({"input": [self.user("."), {"type": "function_call_output", "call_id": "c", "output": "x"}]}))
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": "."}, {"type": "input_image", "image_url": "x"}])]}))
self.assertFalse(is_dot({"input": [{"type": "message", "role": "assistant", "content": "."}]}))
self.assertFalse(is_dot({"input": []}))
def test_local_answer_is_a_complete_response_with_a_final_answer(self):
raw = env["hazir_yaniti"]("ide/x", "cc/opus-5-5 hazır.").decode()
events = [json.loads(line[6:]) for line in raw.splitlines() if line.startswith("data: ")]
self.assertEqual([e["type"] for e in events], ["response.created", "response.output_item.added",
"response.output_text.delta", "response.output_item.done", "response.completed"])
done = events[-1]["response"]
self.assertEqual(done["status"], "completed")
self.assertEqual(done["output"][0]["phase"], "final_answer")
self.assertEqual(done["output"][0]["content"][0]["text"], "cc/opus-5-5 hazır.")
self.assertEqual(done["usage"]["total_tokens"], 0)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,106 @@
"""Bridge 8: "modeli X yap" is handled by the bridge; a per-chat route survives until the user picks another model."""
import tempfile
import unittest
import model_gecis as g
MODELS = [("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("gpt-6.1-sol", "GPT-6.1-Sol"),
("ide/9router-ag-gemini-3.8-flash-low", "agy/gemini-3.8-flash-low"),
("ide/9router-ag-gemini-3.8-flash-high", "agy/gemini-3.8-flash-high"),
("ide/9router-cc-claude-opus-5-5", "cc/opus-5-5"), ("ide/9router-cc-claude-opus-5-5-nt", "cc/opus-5-5 (no-think)"),
("ide/9router-nano-z-ai-glm-5.3-flash-uncensored", "nano/glm-5.3-flash-uncensored"),
("ide/agnes-agnes-2.0-flash", "agnes/agnes-2.0-flash"), ("ide/agnes-agnes-2.5-flash", "agnes/agnes-2.5-flash")]
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
def voice(text):
return user(f"<realtime_delegation>\n <input>{text}</input>\n <transcript_delta>user: x</transcript_delta>\n</realtime_delegation>")
def pick(text):
command = g.komut_coz(text)
return g.eslestir(command["ad"], MODELS) if command and command["tur"] == "gec" else command
class Komut(unittest.TestCase):
def test_spoken_names_resolve_to_one_model(self):
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
("modeli klod opus yap", "ide/9router-cc-claude-opus-5-5"),
("modeli opusa geç", "ide/9router-cc-claude-opus-5-5"),
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]:
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
self.assertEqual(pick("modeli agnes yap"), {"adaylar": ["agnes/agnes-2.0-flash", "agnes/agnes-2.5-flash"]})
def test_ordinary_sentences_are_not_commands(self):
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
"modeli değiştir", "hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
"model " + "çok uzun bir cümle " * 5 + "yap"):
self.assertFalse(pick(text) and "slug" in pick(text), text)
self.assertEqual(g.komut_coz("seçili model hangisi"), {"tur": "sor"})
self.assertEqual(g.komut_coz("bu chatte ana model kim"), {"tur": "sor"})
def test_voice_delegation_yields_only_the_spoken_input(self):
text, index, spoken = g.son_kullanici_metni({"input": [user("a"), voice("modeli opus yap")]})
self.assertEqual((text, index, spoken), ("modeli opus yap", 1, True))
self.assertEqual(g.son_kullanici_metni({"input": [user("<skill>x</skill>")]})[0], None)
self.assertEqual(g.son_kullanici_metni({"input": [{"type": "function_call_output", "output": "x"}]})[0], None)
class TurOrtasi(unittest.TestCase):
def test_message_after_tool_items_is_mid_turn_and_after_final_answer_is_not(self):
final = {"type": "message", "role": "assistant", "phase": "final_answer", "content": "ok"}
call = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
out = {"type": "function_call_output", "call_id": "c", "output": "x"}
self.assertFalse(g.tur_ortasi({"input": [user("a"), final, user(".")]}, 2))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, user(".")]}, 3))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, voice("bekliyorum"), user(".")]}, 4))
self.assertTrue(g.tur_ortasi({"input": [user("a"), {"type": "message", "role": "assistant", "phase": "commentary",
"content": "bakıyorum"}, user(".")]}, 2))
self.assertFalse(g.tur_ortasi({"input": [user(".")]}, 0))
def test_handover_note_replaces_only_the_spoken_input(self):
req = {"input": [voice("modeli opus yap")]}
g.metni_degistir(req, 0, g.devir_notu("cc/opus-5-5", "GPT-6-Sol"))
text = req["input"][0]["content"][0]["text"]
self.assertIn("<transcript_delta>", text)
self.assertIn("cc/opus-5-5 olarak devam ediyorum", text)
self.assertNotIn("modeli opus yap</input>", text)
class Gecmis(unittest.TestCase):
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
voice("modeli opus yap"), user("modeli gpt 6 astra yap")]}
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
self.assertIn("<transcript_delta>", req["input"][2]["content"][0]["text"])
self.assertEqual(req["input"][3]["content"][0]["text"], "modeli gpt 6 astra yap")
self.assertEqual(g.gecmisi_temizle(req, MODELS), 0)
class Yon(unittest.TestCase):
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
with tempfile.TemporaryDirectory() as home:
y = g.Yonlendirme(home)
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
self.assertEqual(g.Yonlendirme(home).al("chat", "gpt-6-sol", simdi=1001), "ide/x") # survives a restart
self.assertIsNone(y.al("other", "gpt-6-sol", simdi=1001))
self.assertIsNone(y.al("chat", "gpt-6-astra", simdi=1002)) # user picked another
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1003)) # and it stays cleared
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
y.koy("chat", "gpt-6-sol", "gpt-6-sol", simdi=1001) # back to the picked one
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1002))
y.koy("old", "a", "b", simdi=1000)
self.assertIsNone(y.al("old", "a", simdi=1000 + g.OMUR_SN + 1))
if __name__ == "__main__":
unittest.main()
@@ -7,12 +7,13 @@ 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"}
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter", "phase_damgala",
"asistan_mesaji_mi"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
env = {"json": json, "log": lambda message: None}
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": __import__("pencere")}
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):
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
nodes.insert(0, node)
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
@@ -162,11 +162,22 @@ class Akis(unittest.TestCase):
class Fren(unittest.TestCase):
def test_ayni_sohbette_ust_uste_ozet_durdurulur(self):
f = o.DonguFreni(sinir=3, pencere_sn=600)
def test_tight_loop_is_stopped_but_spaced_summaries_are_not(self):
f = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
# Summary after summary with no normal request in between: a real loop.
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.assertTrue(f.izin("sohbet-b", 40)) # another conversation is unaffected
# A normal request between summaries (small-window model compacting every turn) keeps working.
g = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
izinler = []
for t in range(0, 70, 10):
izinler.append(g.izin("sohbet-c", t))
g.normal("sohbet-c")
self.assertEqual(izinler, [True] * 7)
self.assertTrue(g.izin("sohbet-c", 70)) # the 8th in the window
g.normal("sohbet-c")
self.assertFalse(g.izin("sohbet-c", 80)) # the total cap still ends a slow loop
self.assertTrue(g.izin("sohbet-c", 700)) # and lifts once the window passes
self.assertIn("Dongu durduruldu", o.DONGU_MESAJI % 3)
@@ -0,0 +1,229 @@
"""Bridge version 7: real context windows, phase stamping, overflow errors, images for vision models."""
import ast
import json
import os
import tempfile
import unittest
from pathlib import Path
import ide_catalog as c
import pencere
import resim_context
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
nodes.insert(0, node)
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
NANO = pencere.nano_sadelestir({"data": [
{"id": "z-ai/glm-5.3-flash-uncensored", "context_length": 1048576,
"capabilities": {"vision": True, "tool_calling": True}},
{"id": "mlabonne/NeuralDaredevil-8B-abliterated", "context_length": 8192,
"capabilities": {"vision": False, "tool_calling": False}},
{"id": "nousresearch/hermes-3-llama-3.1-70b", "context_length": 65536,
"capabilities": {"vision": False, "tool_calling": True}},
{"id": "broken", "context_length": 0}]})
def sse(*events):
return b"".join(b"event: " + e["type"].encode() + b"\ndata: " + json.dumps(e).encode() + b"\n\n" for e in events)
def parse(raw):
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
def message(text, ident="msg_1"):
return {"type": "message", "id": ident, "role": "assistant", "content": [{"type": "output_text", "text": text}]}
class Windows(unittest.TestCase):
def test_provider_data_local_table_and_cap(self):
coz = pencere.coz
# The provider that carries the request decides; nothing is declared above the summarizer's window.
self.assertEqual(coz("9router-nano-z-ai-glm-5.3-flash-uncensored", "model", "nano/z-ai/glm-5.3-flash-uncensored",
nano=NANO)["pencere"], pencere.TAVAN)
small = coz("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "model",
"nano/mlabonne/NeuralDaredevil-8B-abliterated", nano=NANO)
self.assertEqual((small["pencere"], small["arac"], small["gorsel"]), (8192, False, False))
self.assertEqual(coz("9router-cc-claude-opus-5-5", "model", "cc/claude-opus-5-5")["pencere"], 200000)
self.assertEqual(coz("9router-cx-gpt-6-astra", "model", "cx/gpt-6-astra")["pencere"], 272000)
self.assertEqual(coz("no-think-9router-cc-claude-opus-5-5", "model", "no-think/claude-opus-5-5")["pencere"], 200000)
unknown = coz("agnes-agnes-2.5-flash", "model", "Agnes 2.5 Flash", nano=NANO)
self.assertEqual((unknown["pencere"], unknown["kaynak"]), (pencere.VARSAYILAN, "varsayilan"))
self.assertNotIn("broken", NANO)
def test_combo_declares_its_smallest_member(self):
combo = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "cx/gpt-6-astra"},
{"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
{"kind": "model", "model": "nano/nousresearch/hermes-3-llama-3.1-70b"}]}]}}
self.assertEqual(pencere.coz("X", "combo", "X", combo, NANO)["pencere"], 65536)
self.assertEqual(pencere.coz("X", "combo", "X", None, NANO)["pencere"], pencere.VARSAYILAN)
def test_limit_learned_from_the_target_error_only_shrinks(self):
home = tempfile.mkdtemp()
text = "This model's maximum context length is 32768 tokens. However, you requested 51234 tokens"
self.assertTrue(pencere.tasma_mi(text))
self.assertEqual(pencere.sinir_oku(text), 32768)
self.assertEqual(pencere.sinir_oku("prompt is too long: 3040251 tokens > 1000000 maximum"), 1000000)
gateway = ("Your prompt exceeds the model's context length. Estimated input tokens: 171016. "
"Max context tokens: 65536.")
self.assertTrue(pencere.tasma_mi(gateway))
self.assertEqual(pencere.sinir_oku(gateway), 65536)
self.assertIsNone(pencere.sinir_oku("rate limit exceeded"))
self.assertFalse(pencere.tasma_mi("rate limit exceeded"))
self.assertTrue(pencere.ogren(home, "m", 32768))
self.assertFalse(pencere.ogren(home, "m", 65536)) # a larger number never overrides
self.assertEqual(pencere.ogrenilen(home), {"m": 32768})
self.assertEqual(pencere.coz("m", "model", "cc/claude-opus-5-5", ogr=pencere.ogrenilen(home))["pencere"], 32768)
def test_nano_cache_survives_a_network_failure(self):
home = tempfile.mkdtemp()
fresh = {"data": [{"id": "A/b", "context_length": 16384, "capabilities": {}}]}
self.assertEqual(pencere.nano_yukle(home, simdi=1000, getir=lambda: fresh)["a/b"]["pencere"], 16384)
def fail():
raise OSError("offline")
later = 1000 + pencere.NANO_OMUR_SN + 5
self.assertEqual(pencere.nano_yukle(home, simdi=later, getir=fail)["a/b"]["pencere"], 16384)
self.assertEqual(pencere.nano_yukle(tempfile.mkdtemp(), simdi=later, getir=fail), {})
self.assertEqual((pencere.kisa(8192), pencere.kisa(131072), pencere.kisa(200000)), ("8K", "128K", "200K"))
def test_description_line_warns_about_small_window_and_no_tools(self):
bilgi = {"pencere": 8192, "arac": False, "gorsel": False}
line = c.aciklama("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "9R · nano/NeuralDaredevil-8B-abliterated",
"model", None, bilgi)
self.assertEqual(line, "NanoGPT · 8K · araçsız") # warnings first; the rest no longer fits in 30
self.assertLessEqual(len(line), c.ACIKLAMA_SINIRI)
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model", None,
{"pencere": 200000, "arac": True, "gorsel": True}), "Tek model · Claude")
class Guard(unittest.TestCase):
def req(self, old, last="son soru"):
return {"instructions": "talimat", "input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": old}]},
{"type": "function_call_output", "call_id": "c", "output": "cikti " * 4000},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": last}]}]}
def test_request_that_fits_is_untouched(self):
req = self.req("kisa")
before = json.dumps(req)
self.assertEqual(pencere.sigdir(req, 272000)[:2], (0, True))
self.assertEqual(json.dumps(req), before)
def test_oversized_old_parts_lose_their_middle_and_the_last_user_message_never_does(self):
req = self.req("BAS " + "dolgu " * 60000 + " SON", last="yeni " * 3000)
kirpilan, sigdi, tahmin = pencere.sigdir(req, 65536)
self.assertTrue(sigdi)
self.assertGreaterEqual(kirpilan, 1)
old = req["input"][0]["content"][0]["text"]
self.assertTrue(old.startswith("BAS ") and old.endswith(" SON"))
self.assertIn("çıkarıldı (köprü)", old)
self.assertEqual(req["input"][2]["content"][0]["text"], "yeni " * 3000) # the new message is intact
self.assertLessEqual(tahmin, int(65536 * pencere.DOLULUK))
def test_a_last_message_larger_than_the_window_is_reported_not_sent(self):
req = self.req("kisa", last="x" * 400000)
self.assertFalse(pencere.sigdir(req, 32768)[1])
class Phase(unittest.TestCase):
def run_stream(self, *events, chunk=37):
adapter = env["SseRewriter"](set())
raw = sse(*events)
out = b"".join(adapter.feed(raw[i:i + chunk]) for i in range(0, len(raw), chunk)) + adapter.flush()
return parse(out)
def test_message_before_a_tool_call_is_commentary(self):
call = {"type": "function_call", "id": "fc_1", "call_id": "call_1", "name": "shell", "arguments": "{}"}
out = self.run_stream(
{"type": "response.output_item.added", "item": message("")},
{"type": "response.output_text.delta", "item_id": "msg_1", "delta": "bakiyorum"},
{"type": "response.output_item.done", "item": message("bakiyorum")},
{"type": "response.output_item.added", "item": call},
{"type": "response.output_item.done", "item": call},
{"type": "response.completed", "response": {"output": [message("bakiyorum"), call]}})
self.assertEqual([e["type"] for e in out], [
"response.output_item.added", "response.output_text.delta", "response.output_item.done",
"response.output_item.added", "response.output_item.done", "response.completed"]) # order kept
self.assertEqual(out[2]["item"]["phase"], "commentary")
self.assertEqual(out[5]["response"]["output"][0]["phase"], "commentary")
self.assertNotIn("phase", out[5]["response"]["output"][1])
def test_last_message_without_tool_calls_is_the_final_answer(self):
out = self.run_stream(
{"type": "response.output_item.done", "item": message("ara", "msg_a")},
{"type": "response.output_item.added", "item": message("", "msg_b")},
{"type": "response.output_item.done", "item": message("son", "msg_b")},
{"type": "response.completed", "response": {"output": [message("ara", "msg_a"), message("son", "msg_b")]}})
done = [e for e in out if e["type"] == "response.output_item.done"]
self.assertEqual([e["item"]["phase"] for e in done], ["commentary", "final_answer"])
self.assertEqual([o["phase"] for o in out[-1]["response"]["output"]], ["commentary", "final_answer"])
self.assertEqual(out[-1]["type"], "response.completed") # done comes first
def test_existing_phase_and_cut_streams_are_left_alone(self):
native = dict(message("x"), phase="final_answer")
out = self.run_stream({"type": "response.output_item.done", "item": native},
{"type": "response.completed", "response": {"output": [dict(native)]}})
self.assertEqual(out[0]["item"]["phase"], "final_answer")
# No terminal event: the held message is still delivered, unmarked rather than guessed.
cut = self.run_stream({"type": "response.output_item.done", "item": message("yarim")})
self.assertEqual(len(cut), 1)
self.assertNotIn("phase", cut[0]["item"])
def test_overflow_failure_gets_the_code_codex_understands(self):
seen = []
adapter = env["SseRewriter"](set(), hedef="model-x", tasma=lambda hedef, metin: seen.append((hedef, metin)))
failed = {"type": "response.failed", "response": {"error": {
"code": "server_error", "message": "maximum context length is 32768 tokens"}}}
out = parse(adapter.feed(sse(failed)) + adapter.flush())
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
self.assertEqual(seen[0][0], "model-x")
other = {"type": "response.failed", "response": {"error": {"code": "rate_limit", "message": "slow down"}}}
self.assertEqual(parse(adapter.feed(sse(other)))[0]["response"]["error"]["code"], "rate_limit")
class GatewayErrors(unittest.TestCase):
def test_permanent_gateway_error_becomes_a_failure_codex_shows_at_once(self):
adapter = env["SseRewriter"](set())
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
out = parse(adapter.feed(raw) + adapter.flush())
self.assertEqual(out[0]["type"], "response.failed")
self.assertEqual(out[0]["response"]["error"]["code"], "insufficient_quota")
self.assertIn("upstream 402", out[0]["response"]["error"]["message"])
# A transient error is left alone so that Codex retries it.
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
class Images(unittest.TestCase):
def req(self):
img = {"type": "input_image", "image_url": "data:image/png;base64," + "A" * 2000}
return {"input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "bak"}, dict(img)]},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "OKUDUM"}]},
{"type": "function_call_output", "call_id": "c", "output": [dict(img)]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "ne yaziyordu"}]}]}
def test_text_model_gets_notes_vision_model_keeps_user_images(self):
text_only = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(text_only)[0], 2)
self.assertNotIn("input_image", json.dumps(text_only))
vision = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(vision, gorsel=True)[0], 1) # only the tool output
self.assertEqual(vision["input"][0]["content"][1]["type"], "input_image")
self.assertNotIn("input_image", json.dumps(vision["input"][2]))
big = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(big, tut_bayt=500, gorsel=True)[0], 2) # over budget
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,74 @@
"""An answer cut by the client is carried to the next turn of the same conversation, once."""
import json
import unittest
import yarim
def delta(text):
return (b"event: response.output_text.delta\ndata: "
+ json.dumps({"type": "response.output_text.delta", "delta": text}).encode() + b"\n\n")
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
class Yarim(unittest.TestCase):
def setUp(self):
yarim._KAYIT.clear()
def cut(self, key="sohbet-1", text="1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"):
izle = yarim.Izleyici(key, "ide/x")
raw = delta(text[:20]) + delta(text[20:])
for i in range(0, len(raw), 17):
izle.feed(raw[i:i + 17])
return izle
def test_cut_answer_reaches_the_next_user_message_once(self):
izle = self.cut()
self.assertTrue(yarim.kaydet(izle, simdi=100))
req = {"input": [user("liste yaz"), user("kaldigin yerden devam et")]}
self.assertGreater(yarim.uygula(req, "sohbet-1", simdi=200), 0)
note = req["input"][-1]["content"][-1]["text"]
self.assertIn("3. uc", note)
self.assertIn('model="ide/x"', note)
self.assertEqual(req["input"][-1]["content"][0]["text"], "kaldigin yerden devam et") # the user's words stay first
again = {"input": [user("bir daha")]}
self.assertEqual(yarim.uygula(again, "sohbet-1", simdi=300), 0) # used once
self.assertEqual(yarim.uygula({"input": [user("x")]}, "baska-sohbet", simdi=300), 0)
def test_finished_short_or_keyless_answers_are_not_kept(self):
done = self.cut()
done.feed(b'event: response.completed\ndata: {"type": "response.completed"}\n\n')
self.assertFalse(yarim.kaydet(done))
self.assertFalse(yarim.kaydet(self.cut(text="kisa")))
self.assertFalse(yarim.kaydet(self.cut(key="")))
def test_note_waits_for_a_real_user_turn_and_skips_text_codex_already_saved(self):
yarim.kaydet(self.cut(), simdi=100)
tool_turn = {"input": [user("x"), {"type": "function_call_output", "call_id": "c", "output": "ok"}]}
self.assertEqual(yarim.uygula(tool_turn, "sohbet-1", simdi=110), 0)
compaction = {"input": [user("x"), {"type": "compaction_trigger"}]}
self.assertEqual(yarim.uygula(compaction, "sohbet-1", simdi=110), 0)
self.assertIn("sohbet-1", yarim._KAYIT) # still waiting
saved = {"input": [{"type": "message", "role": "assistant", "content": [
{"type": "output_text", "text": "1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"}]}, user("devam")]}
self.assertEqual(yarim.uygula(saved, "sohbet-1", simdi=120), 0)
self.assertNotIn("sohbet-1", yarim._KAYIT)
yarim.kaydet(self.cut(), simdi=100)
self.assertEqual(yarim.uygula({"input": [user("gec")]}, "sohbet-1", simdi=100 + yarim.OMUR_SN + 1), 0) # expired
def test_long_text_keeps_head_and_tail(self):
yarim.kaydet(self.cut(text="BAS" + "x" * 40000 + "SON"), simdi=1)
req = {"input": [user("devam")]}
yarim.uygula(req, "sohbet-1", simdi=2)
note = req["input"][-1]["content"][-1]["text"]
self.assertLess(len(note), yarim.EN_COK_KARAKTER + 600)
self.assertIn("BAS", note)
self.assertTrue(note.rstrip().endswith("</yarim_cevap>"))
self.assertIn("SON\n</yarim_cevap>", note)
if __name__ == "__main__":
unittest.main()
+112
View File
@@ -0,0 +1,112 @@
"""Yarida kesilen cevabi sonraki modele tasir (10 Ekim 2026).
Olcum: Codex kapanir ya da kullanici cevabi durdurursa o ana kadar yazilan metin sohbet gecmisine girmiyor;
sonraki tur (ayni ya da baska modelle) "onceki cevabi goremiyorum" diyor. Kopru akisi zaten goruyor:
ISTEMCI baglantiyi kapattiginda (yanit bitmeden) o ana kadarki metni bellekte tutar ve ayni sohbetin
sonraki isteginde, kullanicinin yeni mesajina kisa bir not olarak ekler. Bir kez kullanilir.
Bilerek yapilmayanlar:
- Ust kaynak kopmasinda (istemci hala bagli) kayit yok: Codex o istegi kendisi yeniden gonderir; not eklenirse
model bastan yazmak yerine ortadan devam eder ve cevabin basi kaybolur.
- Yarim metin asistan mesaji olarak eklenmez: Claude "asistan mesajiyla biten istek"i reddeder ve abonelik
yolunda kimliksiz asistan ogesi risklidir. Kullanici mesajina eklenen not her saglayicida gecerlidir.
- Diske yazilmaz; kopru yeniden baslarsa kayit gider.
"""
import json
import threading
import time
OMUR_SN = 6 * 3600
EN_COK_KAYIT = 50
EN_COK_KARAKTER = 12_000 # nota konan yarim metnin ust siniri (basi ve sonu korunur)
EN_AZ_KARAKTER = 40 # bundan kisa yarim metin tasinmaya degmez
NOT_BASI = "[Köprü notu: bir önceki cevabın yarıda kesildi ve sohbet geçmişine kaydedilmedi."
_KILIT = threading.Lock()
_KAYIT = {} # sohbet anahtari -> {"zaman", "metin", "model"}
class Izleyici:
"""Akistan gecen parcalardan asistan metnini toplar; yanitin bitip bitmedigini izler."""
def __init__(self, anahtar, model):
self.anahtar, self.model = anahtar, model
self.buf = b""
self.parcalar = []
self.bitti = False
def feed(self, chunk):
self.buf += chunk
while b"\n\n" in self.buf:
blok, self.buf = self.buf.split(b"\n\n", 1)
if b"response.output_text.delta" in blok:
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
d = json.loads(satir[5:])
except ValueError:
continue
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
self.parcalar.append(d["delta"])
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
self.bitti = True
@property
def metin(self):
return "".join(self.parcalar)
def kaydet(izleyici, simdi=None):
"""Istemci kapatti ve yanit bitmemisti: yarim metni sakla. True: saklandi."""
metin = izleyici.metin.strip()
if not izleyici.anahtar or izleyici.bitti or len(metin) < EN_AZ_KARAKTER:
return False
simdi = time.time() if simdi is None else simdi
with _KILIT:
_KAYIT[izleyici.anahtar] = {"zaman": simdi, "metin": metin, "model": izleyici.model}
for eski in sorted(_KAYIT, key=lambda k: _KAYIT[k]["zaman"])[:max(0, len(_KAYIT) - EN_COK_KAYIT)]:
del _KAYIT[eski]
return True
def _kisalt(metin):
if len(metin) <= EN_COK_KARAKTER:
return metin
bas, son = EN_COK_KARAKTER // 4, EN_COK_KARAKTER - EN_COK_KARAKTER // 4
return metin[:bas] + f"\n[... ortadan {len(metin) - bas - son} karakter çıkarıldı ...]\n" + metin[-son:]
def uygula(req, anahtar, simdi=None):
"""Ayni sohbetin sonraki isteginde yarim metni kullanicinin son mesajina not olarak ekler.
Donus: eklenen karakter sayisi (0: eklenmedi). Kayit yalniz basariyla eklenince ya da eskiyince silinir."""
if not anahtar:
return 0
simdi = time.time() if simdi is None else simdi
with _KILIT:
kayit = _KAYIT.get(anahtar)
if kayit and simdi - kayit["zaman"] > OMUR_SN:
del _KAYIT[anahtar]
kayit = None
girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi:
return 0
son = girdi[-1]
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"]
ham = json.dumps(girdi, ensure_ascii=False)
if json.dumps(metin[:200], ensure_ascii=False)[1:-1] in ham:
with _KILIT: # Codex bu metni gecmise kendisi yazmis: ikinci kez eklenmez
_KAYIT.pop(anahtar, None)
return 0
notu = (f"{NOT_BASI} Kesilmeden önce yazdığın metin aşağıda. Kullanıcı devam etmeni isterse baştan yazma, "
f"kaldığın yerden sürdür.]\n<yarim_cevap model=\"{kayit['model']}\">\n{_kisalt(metin)}\n</yarim_cevap>")
icerik = son.get("content")
if isinstance(icerik, str):
son["content"] = [{"type": "input_text", "text": icerik}, {"type": "input_text", "text": notu}]
elif isinstance(icerik, list):
icerik.append({"type": "input_text", "text": notu})
else:
return 0
with _KILIT:
_KAYIT.pop(anahtar, None)
return len(notu)
@@ -230,7 +230,7 @@ describe('bridge configuration and command', () => {
routes: {},
nine_router_catalog: { enabled: false },
pano_kanca_user: false,
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
})
})
+2 -2
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '6'
export const CODEX_BRIDGE_VERSION = '8'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
@@ -339,7 +339,7 @@ export function codexBridgeConfig() {
routes: {},
nine_router_catalog: { enabled: false },
pano_kanca_user: false,
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ export interface CodexLinkDeps {
// The single wait this feature owns: how long a freshly spawned bridge gets to
// answer its status endpoint before the apply gives up.
export const CODEX_BRIDGE_START_BUDGET_MS = 10_000
export const CODEX_BRIDGE_START_BUDGET_MS = 45_000
const START_POLL_INTERVAL_MS = 500
// A probe of an already-running (or absent) local bridge. Loopback answers or
// refuses at once; the bound only keeps Settings from hanging on a wedged port.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.25",
"version": "1.0.0-beta.27",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.25",
"version": "1.0.0-beta.27",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",