diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py
index 106cc36..42495c1 100644
--- a/apps/desktop/assets/codex-bridge/bridge.py
+++ b/apps/desktop/assets/codex-bridge/bridge.py
@@ -15,7 +15,7 @@ Data directory (bridge.json, log/): --home
or AITURK_CODEX_BRIDGE_HOME,
otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME.
Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop.
"""
-import http.server, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
+import http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
from gemini_context import coalesce_context
try:
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
@@ -502,6 +502,25 @@ class SseRewriter:
if d is not None and not d["item"].get("phase"):
d["item"]["phase"] = phase
return d
+ def _gorunur_hata(self, mesaj):
+ """Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026)
+ hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda
+ kullanici hatayi duyar."""
+ damga = "%d" % int(time.time() * 1000)
+ rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga
+ oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
+ "content": [{"type": "output_text", "text": mesaj, "annotations": []}]}
+ kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
+ "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
+ return [
+ {"type": "response.output_item.added", "output_index": 0,
+ "item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}},
+ {"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj},
+ {"type": "response.output_item.done", "output_index": 0, "item": oge},
+ {"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
+ "output": [oge], "usage": kullanim}},
+ ]
+
def _sirala(self, d):
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
t = d.get("type", "")
@@ -517,10 +536,7 @@ class SseRewriter:
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."}}}]
+ return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
if self.dolu and pencere.kapali_hata_mi(metin):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
@@ -535,11 +551,12 @@ class SseRewriter:
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
if re.search(r"(? 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")]
+ return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
+ if any(k.startswith(OLUMSUZ) for k in kelimeler):
+ return None
+ fiil = [k for k in kelimeler if k in FIILLER]
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)]
@@ -131,15 +140,33 @@ def eslestir(ad, modeller):
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))
+ kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
+ hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
+ puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
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]:
+ harfler = [s for s in ad if not s.isdigit()]
+ en_harf = max(p[4] for p in puanli)
+ if en_harf < len(harfler):
+ return None # adin bir sozcugu hicbir modelde yok: komut degil
+ iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3]))
+ if not iyi:
+ return None
+ if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme
+ return {"adaylar": [p[3] for p in iyi[:4]]}
+ if len(iyi) == 1:
return {"slug": iyi[0][2], "ad": iyi[0][3]}
+ tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model
+ if tam:
+ # Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan,
+ # sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur.
+ def oncelik(p):
+ return (p[2].startswith("ide/"), -p[5], "/" not in p[3])
+ tam.sort(key=oncelik)
+ if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]):
+ return {"slug": tam[0][2], "ad": tam[0][3]}
+ return {"adaylar": [p[3] for p in tam[:4]]}
return {"adaylar": [p[3] for p in iyi[:4]]}
diff --git a/apps/desktop/assets/codex-bridge/pencere.py b/apps/desktop/assets/codex-bridge/pencere.py
index b879276..f4ecbd8 100644
--- a/apps/desktop/assets/codex-bridge/pencere.py
+++ b/apps/desktop/assets/codex-bridge/pencere.py
@@ -146,12 +146,22 @@ def kapali_hata_mi(metin):
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
+_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
+
+
+def hiz_siniri_mi(metin):
+ """Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
+ return bool(_HIZ_SINIRI.search(metin or ""))
+
+
def tasma_mi(metin):
- return bool(_TASMA.search(metin or ""))
+ return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
def sinir_oku(metin):
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
+ if hiz_siniri_mi(metin):
+ return None
for kalip in _SINIR:
m = kalip.search(metin or "")
if m and int(m.group(1)) >= TABAN:
diff --git a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py
index 87490a7..590d47c 100644
--- a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py
+++ b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py
@@ -32,8 +32,11 @@ class Ek(unittest.TestCase):
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"])
+ # A permanent error ends the turn with a visible answer instead of a silent retry.
+ self.assertEqual([e["type"] for e in out], ["response.output_item.added", "response.output_text.delta",
+ "response.output_item.done", "response.completed"])
+ self.assertIn("araç çağrısını desteklemiyor", out[1]["delta"])
+ self.assertEqual(out[2]["item"]["phase"], "final_answer")
self.assertEqual(seen, ["m"])
home = tempfile.mkdtemp()
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
diff --git a/apps/desktop/assets/codex-bridge/test_model_gecis.py b/apps/desktop/assets/codex-bridge/test_model_gecis.py
index d0734e7..2491014 100644
--- a/apps/desktop/assets/codex-bridge/test_model_gecis.py
+++ b/apps/desktop/assets/codex-bridge/test_model_gecis.py
@@ -29,14 +29,24 @@ 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 klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
+ ("modeli opus 5 5'e 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"]})
+ self.assertIn("adaylar", pick("modeli klod opus yap")) # two Opus entries: ask
+ self.assertIn("adaylar", pick("modeli sol yap")) # GPT-6-Sol or GPT-6.1-Sol: ask
+ self.assertIn("adaylar", pick("modeli gpt 999 astra yap")) # words fit, version does not: ask
+
+ def test_negations_and_general_questions_are_not_commands(self):
+ for text in ("modeli gemini flash low yapma", "modeli değiştirme opus kalsın", "GPT 6 Astra modeli için test yap",
+ "hangi model daha ucuz", "en iyi model hangisi", "modeli seçili dosyaya göre gemini flash low yap"):
+ result = pick(text)
+ self.assertFalse(result and ("slug" in result or result.get("tur") == "sor"), text)
+ self.assertEqual(g.komut_coz("hangi modelsin"), {"tur": "sor"})
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",
@@ -77,7 +87,7 @@ class TurOrtasi(unittest.TestCase):
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")]}
+ voice("modeli opus 5 5 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")
diff --git a/apps/desktop/assets/codex-bridge/test_pencere_phase.py b/apps/desktop/assets/codex-bridge/test_pencere_phase.py
index 8f71c46..bbb3109 100644
--- a/apps/desktop/assets/codex-bridge/test_pencere_phase.py
+++ b/apps/desktop/assets/codex-bridge/test_pencere_phase.py
@@ -17,7 +17,7 @@ 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}
+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)
NANO = pencere.nano_sadelestir({"data": [
@@ -196,9 +196,13 @@ class GatewayErrors(unittest.TestCase):
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"])
+ self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
+ self.assertIn("kredi ya da kota", out[1]["delta"])
+ self.assertIn("upstream 402", out[1]["delta"])
+ raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 400"}})
+ out = parse(env["SseRewriter"](set()).feed(raw))
+ self.assertIn("nedenini bildirmedi", out[1]["delta"])
+ self.assertEqual(out[-1]["type"], "response.completed")
# 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")
@@ -238,6 +242,11 @@ class ClaudeOrani(unittest.TestCase):
self.assertEqual((trimmed, fits), (1, True))
self.assertLess(estimate, 194_000)
self.assertTrue(p.kapali_hata_mi("upstream 400"))
+ # A per-minute rate limit is neither an overflow nor a window size to learn.
+ rate = "Rate limit reached: limit of 32768 tokens per minute, context window ok"
+ self.assertFalse(p.tasma_mi(rate))
+ self.assertIsNone(p.sinir_oku(rate))
+ self.assertTrue(p.tasma_mi("maximum context length is 65536 tokens"))
self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema"))
if __name__ == "__main__":
diff --git a/apps/desktop/assets/codex-bridge/yarim.py b/apps/desktop/assets/codex-bridge/yarim.py
index 83a5fc6..93a87e3 100644
--- a/apps/desktop/assets/codex-bridge/yarim.py
+++ b/apps/desktop/assets/codex-bridge/yarim.py
@@ -48,7 +48,15 @@ class Izleyici:
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
+ # Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
+ for satir in blok.split(b"\n"):
+ if satir.startswith(b"data:"):
+ try:
+ tur = json.loads(satir[5:]).get("type")
+ except (ValueError, AttributeError):
+ continue
+ if tur in ("response.completed", "response.failed", "response.incomplete"):
+ self.bitti = True
@property
def metin(self):
@@ -89,7 +97,12 @@ def uygula(req, anahtar, simdi=None):
girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi:
return 0
- son = girdi[-1]
+ 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 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"]