diff --git a/apps/desktop/assets/codex-bridge/README.md b/apps/desktop/assets/codex-bridge/README.md index ffa9140..54d8ee1 100644 --- a/apps/desktop/assets/codex-bridge/README.md +++ b/apps/desktop/assets/codex-bridge/README.md @@ -10,6 +10,13 @@ Codex depends on it while `openai_base_url` points here. key read from the agent's `.env` on every catalog refresh (never written elsewhere). - `ide_catalog.py`: which IDE models and combos are added to Codex's model list and how they are named (`provider/model`, combos by bare name). + The public `/models` catalog supplies every OmniRoute chat model; a member does + not need a private local model allowlist. `/router/catalog` supplies only the + `kind` metadata used for sorting, never additional model IDs. Direct 9Router + entries and internal 9Router combos are excluded. Combos sort A–Z first, then + individual models by provider and name, including OpenAI subscription models. + Codex labels the first visible row `isDefault`; a saved model selection remains + unchanged. A failed or empty refresh keeps the last successful catalog. - `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made by one provider stays readable after the user switches to another. - `gemini_context.py`, `resim_context.py`: request fixes for routed models. diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index 7547bff..490b90a 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -26,7 +26,7 @@ from resim_context import resimleri_sadelestir import ide_catalog import ozet_kopru -SURUM = "4" +SURUM = "5" HERE = os.path.dirname(os.path.abspath(__file__)) # Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older @@ -183,14 +183,8 @@ def load_ide_catalog(): ROUTES[IDE_ROUTE] = {"url": base + "/responses", "key": anahtar, "flatten_tools": True} clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra") levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels") - # Secicide yalniz "saglayici/model" gorunur. 9Router listesinde ayni adla zaten duran model - # ikinci kez eklenmez (ayni modelin iki satir olmamasi icin). + # Gorunen adlari ayirt et; farkli kimlikteki modeller adlari benziyor diye silinmez. var_olan = {m.get("display_name", "").lower() for m in extra_models} - for g in girdiler: - slug = IDE_PREFIX + g["id"] - g["kisa"] = ide_catalog.kisa_ad(g["ad"]) - # "ad (2)": ayni modelin gecitteki ikinci yolu; taban ad listede varsa o da eklenmez. - girdiler = [g for g in girdiler if re.sub(r" \(\d+\)$", "", g["kisa"]).lower() not in var_olan] adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan) for g in girdiler: slug = IDE_PREFIX + g["id"] @@ -198,7 +192,8 @@ def load_ide_catalog(): continue extra_models.append({"slug": slug, "display_name": adlar[g["id"]], "description": f"AITURK IDE: {g['ad']}", "route": IDE_ROUTE, "target": g["id"], - "clone_from": clone, "priority": 300 + n, "reasoning_levels": levels}) + "clone_from": clone, "priority": 300 + n, "reasoning_levels": levels, + "kind": g["kind"]}) model_map[slug] = (IDE_ROUTE, g["id"], None) n += 1 else: @@ -724,6 +719,8 @@ class H(http.server.BaseHTTPRequestHandler): if not m.get("base_instructions") and not has_tmpl: m.update(saved) models.append(m); changed += 1 + if EXTRA_MODELS: + ide_catalog.sirala_codex(models, EXTRA_MODELS) out = json.dumps(data, ensure_ascii=False).encode("utf-8") except Exception as e: log(f"{tag} model listesi degistirilemedi: {e}") diff --git a/apps/desktop/assets/codex-bridge/ide_catalog.py b/apps/desktop/assets/codex-bridge/ide_catalog.py index b7261d2..6c3ed73 100644 --- a/apps/desktop/assets/codex-bridge/ide_catalog.py +++ b/apps/desktop/assets/codex-bridge/ide_catalog.py @@ -6,9 +6,8 @@ anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazi Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json {"enabled": true, "kapsam": "ide"} Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik). -kapsam "ide" : IDE ayarinda tanimli modeller + 9Router'a eklenmis saglayicilarin modelleri + - adlandirilmis combolar (varsayilan) -kapsam "hepsi" : gecitteki butun sohbet modelleri (1000+; Codex listesi cok buyur) +kapsam "ide" ve "hepsi": gecidin uye katalogundaki OmniRoute sohbet modelleri. +Yerel config'teki model adlari uye katalogunu sinirlamaz. """ import json, os, re, urllib.request @@ -125,10 +124,10 @@ def adlandir(girdiler, dolu=()): return sonuc -def sec(data, tanimli, kapsam="ide"): - """Gecit katalogundan Codex'e eklenecek girdiler: [{'id','ad'}], sirali ve tekrarsiz. +def sec(data, tanimli=(), kapsam="ide", turler=None): + """Gecidin uye katalogundan girdiler: [{'id','ad','kind'}], sirali ve tekrarsiz. Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez.""" - tanimli = {a.lower() for a in tanimli if a} + turler = turler or {} secilen, gorulen = [], set() for m in data: mid = m.get("id") or "" @@ -136,20 +135,16 @@ def sec(data, tanimli, kapsam="ide"): if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]): continue gorunen = ts.get("display_name") or mid - kisa = gorunen.split(" · ")[-1].strip() - combo = "/" not in kisa # saglayici/model bicimi degil: adlandirilmis combo ya da takma ad - # 9Router'dan gelen combolar Codex'te gosterilmez (kullanici karari, 9 Ekim 2026): dogrudan - # 9Router kaynagi ve gecidin "9router" saglayicisi uzerinden gelen kopyalari. - if combo and (ts.get("source") == "9router" or mid.lower().startswith("9router-")): + kind = turler.get(mid) or ts.get("kind") or "model" + # Tek uye yolu: 9Router -> OmniRoute -> TurkServis -> Codex. + # Dogrudan 9Router satirlari ayni modelin ikinci kopyasi olarak eklenmez. + if ts.get("source") != "omniroute": continue - # 9Router'a eklenmis saglayicilarin tekil modelleri her zaman eklenir (kullanici karari, 9 Ekim 2026: - # "9Router'in combosu olmasin dedim, 9Router'in modeli olsun"). Ayni modelin OmniRoute uzerinden - # gelen kopyasi yalniz IDE ayarinda tanimliysa eklenir; boylece her model listede bir kez durur. - dokuz_model = not combo and ts.get("source") == "9router" - if kapsam != "hepsi" and not combo and not dokuz_model and kisa.lower() not in tanimli and mid.lower() not in tanimli: + # Combo adi biciminden tahmin edilmez; duz adli Agnes/Bekci modelleri de vardir. + if kind == "combo" and mid.lower().startswith("9router-"): continue gorulen.add(mid) - secilen.append({"id": mid, "ad": gorunen, "sira": ts.get("order") or 0}) + secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0}) 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} @@ -158,7 +153,38 @@ def sec(data, tanimli, kapsam="ide"): 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"]} for s in secilen] + return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"]} for s in secilen] + + +def sirala_codex(modeller, ekler): + """Tum listeyi siralar: combo A-Z, sonra saglayici A-Z / model A-Z. + Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz. + Codex'in isDefault etiketi kendi davranisi geregi ilk gorunur satira verilir.""" + bilgiler = {m["slug"]: m for m in ekler} + + def anahtar(m): + slug = m.get("slug", "") + bilgi = bilgiler.get(slug, {}) + ad = kisa_ad(m.get("display_name") or slug) + if ad.startswith(DOKUZ_ISARET): + ad = ad[len(DOKUZ_ISARET):] + if bilgi.get("kind") == "combo": + return (0, ad.casefold(), "", slug) + if slug.startswith("ide/"): + mid = slug[len("ide/"):] + if mid.startswith("9router-"): + mid = mid[len("9router-"):] + saglayici, _, model = ad.partition("/") + if not model: + saglayici, _, _ = mid.partition("-") + model = ad + else: + saglayici, model = "OpenAI", ad + return (1, saglayici.casefold(), model.casefold(), slug) + + modeller.sort(key=anahtar) + for n, model in enumerate(modeller): + model["priority"] = n def yukle(ua="AstraBridge/1.0 (codex)", timeout=30): @@ -172,5 +198,19 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30): return "anahtar-yok", "", "", [] base, varsayilan, adlar = ide_yapilandirma() req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua}) - data = json.load(urllib.request.urlopen(req, timeout=timeout)).get("data", []) - return "tamam", base, anahtar, sec(data, adlar + [varsayilan], ayar["kapsam"]) + with urllib.request.urlopen(req, timeout=timeout) as resp: + data = json.load(resp).get("data") + if not isinstance(data, list) or not data: + raise ValueError("Gecit bos veya gecersiz model katalogu dondurdu") + req = urllib.request.Request(base + "/router/catalog", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua}) + with urllib.request.urlopen(req, timeout=timeout) as resp: + metadata = json.load(resp).get("models") + if not isinstance(metadata, list) or not metadata: + raise ValueError("Gecit model turlerini dondurmedi") + # Bu ucta uye listesinde olmayan ic kayitlar da bulunur. Yalniz tur bilgisi alinir; + # modele erisim listesi her zaman /models yanitidir. + turler = {m["id"]: m["kind"] for m in metadata if m.get("id") and m.get("kind") in ("combo", "model")} + girdiler = sec(data, adlar + [varsayilan], ayar["kapsam"], turler) + if not girdiler: + raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok") + return "tamam", base, anahtar, girdiler diff --git a/apps/desktop/assets/codex-bridge/test_ide_catalog.py b/apps/desktop/assets/codex-bridge/test_ide_catalog.py index ec172c0..0470fa7 100644 --- a/apps/desktop/assets/codex-bridge/test_ide_catalog.py +++ b/apps/desktop/assets/codex-bridge/test_ide_catalog.py @@ -2,8 +2,8 @@ import json, os, tempfile, unittest import ide_catalog as c -def girdi(mid, ad, yetenek=("chat",), sira=0, kaynak="omniroute"): - return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira, "source": kaynak}} +def girdi(mid, ad, yetenek=("chat",), sira=0, kaynak="omniroute", kind="model"): + return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira, "source": kaynak, "kind": kind}} KATALOG = [ @@ -11,8 +11,8 @@ KATALOG = [ girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2), girdi("9router-nano-x", "OmniRoute · nano/x", sira=1), girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)), - girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9), - girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8), + girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9, kind="combo"), + girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8, kind="combo"), girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"), ] @@ -52,22 +52,22 @@ class Yapilandirma(unittest.TestCase): class Secim(unittest.TestCase): def test_ide_kapsami(self): s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide") - # Tanimli model + adlandirilmis combolar; tanimsiz saglayici modeli ve gorsel modeli yok. - self.assertEqual([x["id"] for x in s], ["9router-cc-opus", "Ajan-Arac"]) + # Uye katalogu yerel config'teki adlarla sinirlanmaz. + self.assertEqual([x["id"] for x in s], ["9router-nano-x", "9router-cc-opus", "Ajan-Arac"]) def test_9router_combolari_gosterilmez(self): katalog = [girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=1), - girdi("9router-Takim-Agir", "Takim-Agir (2)", sira=2), + girdi("9router-Takim-Agir", "Takim-Agir (2)", sira=2, kind="combo"), girdi("Takim-Agir", "Takim-Agir (3)", sira=3), girdi("Ajan-Arac", "Ajan-Arac", sira=4), girdi("9router-nano-x", "nano/x", kaynak="9router", sira=5)] s = c.sec(katalog, ["nano/x"], "ide") # 9Router combosu ve gecidin 9router saglayicisindaki kopyasi yok; OmniRoute'un kendi combosu - # sayacsiz adiyla kalir. 9Router'in tekil modeli (combo degil) etkilenmez. + # sayacsiz adiyla kalir. Dogrudan 9Router tekilleri de eklenmez. self.assertEqual([(x["id"], x["ad"]) for x in s], - [("Takim-Agir", "Takim-Agir"), ("Ajan-Arac", "Ajan-Arac"), ("9router-nano-x", "nano/x")]) + [("Takim-Agir", "Takim-Agir"), ("Ajan-Arac", "Ajan-Arac")]) - def test_9router_saglayici_modelleri_tanimsiz_da_olsa_eklenir(self): + def test_9router_modelleri_yalniz_omniroute_uzerinden_eklenir(self): katalog = [girdi("9router-nano-x-aaaa1111", "nano/x", kaynak="9router", sira=1), girdi("9router-nano-x", "nano/x (2)", sira=2), girdi("9router-ocg-y-bbbb2222", "ocg/y", kaynak="9router", sira=3), @@ -75,10 +75,9 @@ class Secim(unittest.TestCase): girdi("9router-ses", "nano/ses", ("tts",), kaynak="9router", sira=5), girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=6)] s = c.sec(katalog, [], "ide") - # 9Router modelleri IDE ayarinda adi gecmese de gelir; OmniRoute kopyasi, OmniRoute'un kendi - # saglayici modeli, sohbet olmayan model ve 9Router combosu gelmez. + # Yerel model tanimi gerekmez; iki kaynak yerine yalniz OmniRoute yolu gelir. self.assertEqual([(x["id"], x["ad"]) for x in s], - [("9router-nano-x-aaaa1111", "nano/x"), ("9router-ocg-y-bbbb2222", "ocg/y")]) + [("9router-nano-x", "nano/x"), ("vag-z", "vag/z")]) def test_gorunen_ad_yalniz_saglayici_ve_model(self): for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"), diff --git a/apps/desktop/assets/codex-bridge/test_member_catalog.py b/apps/desktop/assets/codex-bridge/test_member_catalog.py new file mode 100644 index 0000000..23f4a11 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_member_catalog.py @@ -0,0 +1,63 @@ +"""The shipped IDE must use the same public OmniRoute catalog as every member.""" +import io +import json +import unittest +from unittest.mock import patch +import ide_catalog as c + + +def row(mid, name, source="omniroute", capabilities=("chat",)): + return {"id": mid, "turkservis": {"display_name": name, "source": source, + "capabilities": list(capabilities)}} + + +class MemberCatalog(unittest.TestCase): + def test_metadata_classifies_only_public_models(self): + public = [row("plain", "Plain"), row("team", "Team")] + metadata = [{"id": "plain", "kind": "model"}, {"id": "team", "kind": "combo"}, + {"id": "internal", "kind": "combo"}] + replies = [io.BytesIO(json.dumps({"data": public}).encode()), + io.BytesIO(json.dumps({"models": metadata}).encode())] + with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \ + patch.object(c, "anahtar_oku", return_value="test-key"), \ + patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \ + patch.object(c.urllib.request, "urlopen", side_effect=replies): + status, _, _, selected = c.yukle() + self.assertEqual(status, "tamam") + self.assertEqual({m["id"]: m["kind"] for m in selected}, {"plain": "model", "team": "combo"}) + + def test_empty_catalog_is_a_refresh_failure(self): + with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \ + patch.object(c, "anahtar_oku", return_value="test-key"), \ + patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \ + patch.object(c.urllib.request, "urlopen", return_value=io.BytesIO(b'{"data": []}')): + with self.assertRaises(ValueError): + c.yukle() + + def test_public_omniroute_models_do_not_need_private_local_configuration(self): + rows = [row("9router-nano-x", "9R · nano/x"), row("agnes-flat", "Agnes Flat"), + row("Ajan-Arac", "Ajan-Arac"), row("image", "image", capabilities=("image",))] + self.assertEqual({m["id"] for m in c.sec(rows, [])}, + {"9router-nano-x", "agnes-flat", "Ajan-Arac"}) + + def test_direct_route_is_never_added_beside_omniroute(self): + rows = [row("9router-nano-x-direct", "9R · nano/x", "9router"), + row("9router-nano-x", "9R · nano/x")] + self.assertEqual([m["id"] for m in c.sec(rows, ["nano/x"], "hepsi")], ["9router-nano-x"]) + + def test_model_order_uses_combo_metadata_and_preserves_every_model(self): + models = [{"slug": "gpt-z", "display_name": "GPT Z", "priority": 1}, + {"slug": "ide/bekci-kurnaz", "display_name": "kurnaz", "priority": 2}, + {"slug": "ide/Z-Team", "display_name": "Z-Team", "priority": 5}, + {"slug": "ide/9router-cc-opus", "display_name": "9R · cc/opus", "priority": 3}, + {"slug": "ide/A-Team", "display_name": "A-Team", "priority": 4}] + extras = [{"slug": m["slug"], "kind": "combo" if m["slug"].endswith("Team") else "model"} + for m in models if m["slug"].startswith("ide/")] + c.sirala_codex(models, extras) + self.assertEqual([m["slug"] for m in models], + ["ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus", "gpt-z"]) + self.assertEqual([m["priority"] for m in models], list(range(5))) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/desktop/electron/codex-link-config.ts b/apps/desktop/electron/codex-link-config.ts index de2cd9c..8586b37 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 = '4' +export const CODEX_BRIDGE_VERSION = '5' 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' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ba87c7c..bc56b9a 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.23", + "version": "1.0.0-beta.24", "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 b84c244..c68e947 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/desktop": { "name": "aiturk-ide", - "version": "1.0.0-beta.23", + "version": "1.0.0-beta.24", "license": "MIT", "dependencies": { "@assistant-ui/core": "0.2.23",