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"]