diff --git a/apps/desktop/assets/codex-bridge/README.md b/apps/desktop/assets/codex-bridge/README.md index 54d8ee1..13a0037 100644 --- a/apps/desktop/assets/codex-bridge/README.md +++ b/apps/desktop/assets/codex-bridge/README.md @@ -26,3 +26,25 @@ Run: `python bridge.py --home --hermes-home `. The IDE wr `POST /aiturk/codex-baglama/stop` ends the process. Tests: `python -m unittest discover` in this directory. + +## 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 " 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. diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index 49e2efb..f5af371 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -25,8 +25,10 @@ except ImportError: from resim_context import resimleri_sadelestir import ide_catalog import ozet_kopru +import pencere +import yarim -SURUM = "6" +SURUM = "7" HERE = os.path.dirname(os.path.abspath(__file__)) # Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older @@ -165,13 +167,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/" 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 +198,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 +212,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 +422,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"(?{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 +1121,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 +1143,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 +1185,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 +1207,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 diff --git a/apps/desktop/assets/codex-bridge/ide_catalog.py b/apps/desktop/assets/codex-bridge/ide_catalog.py index 81d7162..fb4687f 100644 --- a/apps/desktop/assets/codex-bridge/ide_catalog.py +++ b/apps/desktop/assets/codex-bridge/ide_catalog.py @@ -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 diff --git a/apps/desktop/assets/codex-bridge/ozet_kopru.py b/apps/desktop/assets/codex-bridge/ozet_kopru.py index 0e82e11..cac60a4 100644 --- a/apps/desktop/assets/codex-bridge/ozet_kopru.py +++ b/apps/desktop/assets/codex-bridge/ozet_kopru.py @@ -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 diff --git a/apps/desktop/assets/codex-bridge/pencere.py b/apps/desktop/assets/codex-bridge/pencere.py new file mode 100644 index 0000000..119365d --- /dev/null +++ b/apps/desktop/assets/codex-bridge/pencere.py @@ -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 diff --git a/apps/desktop/assets/codex-bridge/resim_context.py b/apps/desktop/assets/codex-bridge/resim_context.py index a389282..33cde0d 100644 --- a/apps/desktop/assets/codex-bridge/resim_context.py +++ b/apps/desktop/assets/codex-bridge/resim_context.py @@ -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: diff --git a/apps/desktop/assets/codex-bridge/test_access_programs.py b/apps/desktop/assets/codex-bridge/test_access_programs.py index cb126e3..54a0bff 100644 --- a/apps/desktop/assets/codex-bridge/test_access_programs.py +++ b/apps/desktop/assets/codex-bridge/test_access_programs.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py index 7f8821f..cba8b02 100644 --- a/apps/desktop/assets/codex-bridge/test_combo_tekrar.py +++ b/apps/desktop/assets/codex-bridge/test_combo_tekrar.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_kopru7_ek.py b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py new file mode 100644 index 0000000..0840b5b --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_kopru7_ek.py @@ -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() diff --git a/apps/desktop/assets/codex-bridge/test_model_switch.py b/apps/desktop/assets/codex-bridge/test_model_switch.py index db19603..a149c2e 100644 --- a/apps/desktop/assets/codex-bridge/test_model_switch.py +++ b/apps/desktop/assets/codex-bridge/test_model_switch.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_ozet_kopru.py b/apps/desktop/assets/codex-bridge/test_ozet_kopru.py index 7a27043..2497f34 100644 --- a/apps/desktop/assets/codex-bridge/test_ozet_kopru.py +++ b/apps/desktop/assets/codex-bridge/test_ozet_kopru.py @@ -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) diff --git a/apps/desktop/assets/codex-bridge/test_pencere_phase.py b/apps/desktop/assets/codex-bridge/test_pencere_phase.py new file mode 100644 index 0000000..da6ae9e --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_pencere_phase.py @@ -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() diff --git a/apps/desktop/assets/codex-bridge/test_yarim.py b/apps/desktop/assets/codex-bridge/test_yarim.py new file mode 100644 index 0000000..ba776c1 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_yarim.py @@ -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("")) + self.assertIn("SON\n", note) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/desktop/assets/codex-bridge/yarim.py b/apps/desktop/assets/codex-bridge/yarim.py new file mode 100644 index 0000000..83a5fc6 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/yarim.py @@ -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\n{_kisalt(metin)}\n") + 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) diff --git a/apps/desktop/electron/codex-link-config.test.ts b/apps/desktop/electron/codex-link-config.test.ts index f520d2c..2fe2ece 100644 --- a/apps/desktop/electron/codex-link-config.test.ts +++ b/apps/desktop/electron/codex-link-config.test.ts @@ -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 } }) }) diff --git a/apps/desktop/electron/codex-link-config.ts b/apps/desktop/electron/codex-link-config.ts index dfdcb6b..0218274 100644 --- a/apps/desktop/electron/codex-link-config.ts +++ b/apps/desktop/electron/codex-link-config.ts @@ -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 = '7' 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 } } } diff --git a/apps/desktop/electron/codex-link.ts b/apps/desktop/electron/codex-link.ts index c5d2081..764b370 100644 --- a/apps/desktop/electron/codex-link.ts +++ b/apps/desktop/electron/codex-link.ts @@ -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. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 56639b3..8ba1e05 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "aiturk-ide", "productName": "AITURK IDE", "private": true, - "version": "1.0.0-beta.25", + "version": "1.0.0-beta.26", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "author": "AITURK / TurkServis", "repository": { diff --git a/package-lock.json b/package-lock.json index d293863..13ef359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/desktop": { "name": "aiturk-ide", - "version": "1.0.0-beta.25", + "version": "1.0.0-beta.26", "license": "MIT", "dependencies": { "@assistant-ui/core": "0.2.23",