Compare commits

..
Author SHA1 Message Date
yilsem 8ee676d7c6 test(codex-link): track the shipped bridge protocol version 2026-10-09 23:21:17 +03:00
yilsem 68a97b7489 ci: pin checkout through verified Gitea mirror 2026-10-09 23:18:39 +03:00
yilsem 6e91154f15 fix(codex-link): use member OmniRoute catalog and metadata ordering 2026-10-09 23:10:29 +03:00
yilsemandClaude Opus 5.5 88e660768c feat(codex-bridge): keep the gateway's short "9R" mark in Codex model names
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 19:27:22 +03:00
yilsemandClaude Opus 5.5 bfd0ff196d feat(codex-bridge): add the models of 9Router's providers to Codex (beta.23)
Codex only received combos and the models named in the IDE config, so none of
the models behind the providers added to 9Router showed up. Those single
models are now always added; 9Router's combos stay hidden. The copy of the
same model that comes through OmniRoute is still added only when the IDE
config names it, so each model is listed once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 19:26:00 +03:00
yilsemandClaude Opus 5.5 1e0f2f9aea feat(codex-bridge): hide 9Router combos in Codex, drop the config backup (beta.22)
Combos that come from 9Router, directly or through the gateway's 9router
provider, are no longer added to Codex's model list; the gateway's own combos
keep their plain names. The IDE no longer leaves a config.toml backup behind:
turning the setting off already restores the file byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 17:51:10 +03:00
11 changed files with 197 additions and 67 deletions

No files matched your search

+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
- uses: actions/checkout@v4 - uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with: with:
github-server-url: https://gitea.twinpay.one github-server-url: https://gitea.twinpay.one
- name: Mobil TLS, profil ve baslatma regresyonlari - name: Mobil TLS, profil ve baslatma regresyonlari
@@ -57,7 +57,7 @@ jobs:
timeout-minutes: 40 timeout-minutes: 40
steps: steps:
- name: Kaynagi al - name: Kaynagi al
uses: actions/checkout@v4 uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with: with:
github-server-url: https://gitea.twinpay.one github-server-url: https://gitea.twinpay.one
@@ -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). 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 - `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). 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 - `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. by one provider stays readable after the user switches to another.
- `gemini_context.py`, `resim_context.py`: request fixes for routed models. - `gemini_context.py`, `resim_context.py`: request fixes for routed models.
+6 -9
View File
@@ -26,7 +26,7 @@ from resim_context import resimleri_sadelestir
import ide_catalog import ide_catalog
import ozet_kopru import ozet_kopru
SURUM = "2" SURUM = "5"
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older # 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} ROUTES[IDE_ROUTE] = {"url": base + "/responses", "key": anahtar, "flatten_tools": True}
clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra") clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra")
levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels") levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels")
# Secicide yalniz "saglayici/model" gorunur. 9Router listesinde ayni adla zaten duran model # Gorunen adlari ayirt et; farkli kimlikteki modeller adlari benziyor diye silinmez.
# ikinci kez eklenmez (ayni modelin iki satir olmamasi icin).
var_olan = {m.get("display_name", "").lower() for m in extra_models} 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) adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan)
for g in girdiler: for g in girdiler:
slug = IDE_PREFIX + g["id"] slug = IDE_PREFIX + g["id"]
@@ -198,7 +192,8 @@ def load_ide_catalog():
continue continue
extra_models.append({"slug": slug, "display_name": adlar[g["id"]], extra_models.append({"slug": slug, "display_name": adlar[g["id"]],
"description": f"AITURK IDE: {g['ad']}", "route": IDE_ROUTE, "target": 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) model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1 n += 1
else: else:
@@ -724,6 +719,8 @@ class H(http.server.BaseHTTPRequestHandler):
if not m.get("base_instructions") and not has_tmpl: if not m.get("base_instructions") and not has_tmpl:
m.update(saved) m.update(saved)
models.append(m); changed += 1 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") out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e: except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}") log(f"{tag} model listesi degistirilemedi: {e}")
+81 -15
View File
@@ -6,8 +6,8 @@ anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazi
Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json
{"enabled": true, "kapsam": "ide"} {"enabled": true, "kapsam": "ide"}
Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik). Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik).
kapsam "ide" : IDE ayarinda tanimli modeller + adlandirilmis combolar (varsayilan) kapsam "ide" ve "hepsi": gecidin uye katalogundaki OmniRoute sohbet modelleri.
kapsam "hepsi" : gecitteki butun sohbet modelleri (1000+; Codex listesi cok buyur) Yerel config'teki model adlari uye katalogunu sinirlamaz.
""" """
import json, os, re, urllib.request import json, os, re, urllib.request
@@ -84,23 +84,33 @@ def ide_yapilandirma(agent=AGENT):
return base.rstrip("/"), varsayilan, adlar return base.rstrip("/"), varsayilan, adlar
DOKUZ_ISARET = "9R · "
def _isaret(ad):
"""Gecidin dogrudan 9Router'dan gelen tekil modele koydugu kisa isaret korunur (kullanici karari,
9 Ekim 2026: "9Router" yazisi uzun, hic yazmamak da sorun; "9R" olarak kisa gorunsun)."""
return DOKUZ_ISARET if (ad or "").startswith(DOKUZ_ISARET) else ""
def kisa_ad(ad): def kisa_ad(ad):
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi """Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki (kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b", saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b",
"OmniRoute · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash", combo adi oldugu gibi kalir.""" "OmniRoute · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash", combo adi oldugu gibi kalir."""
on = _isaret(ad)
ad = (ad or "").split(" · ")[-1].strip() ad = (ad or "").split(" · ")[-1].strip()
parcalar = [p for p in ad.split("/") if p] parcalar = [p for p in ad.split("/") if p]
if len(parcalar) <= 2: if len(parcalar) <= 2:
return "/".join(parcalar) or ad return on + ("/".join(parcalar) or ad)
return parcalar[0] + "/" + parcalar[-1] return on + parcalar[0] + "/" + parcalar[-1]
def adlandir(girdiler, dolu=()): def adlandir(girdiler, dolu=()):
"""[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur; """[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur;
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar.""" yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar."""
kisa = {k: kisa_ad(a) for k, a in girdiler} kisa = {k: kisa_ad(a) for k, a in girdiler}
tam = {k: (a or "").split(" · ")[-1].strip() for k, a in girdiler} tam = {k: _isaret(a) + (a or "").split(" · ")[-1].strip() for k, a in girdiler}
sayac = {} sayac = {}
for v in list(kisa.values()) + list(dolu): for v in list(kisa.values()) + list(dolu):
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1 sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
@@ -114,10 +124,10 @@ def adlandir(girdiler, dolu=()):
return sonuc return sonuc
def sec(data, tanimli, kapsam="ide"): def sec(data, tanimli=(), kapsam="ide", turler=None):
"""Gecit katalogundan Codex'e eklenecek girdiler: [{'id','ad'}], sirali ve tekrarsiz. """Gecidin uye katalogundan girdiler: [{'id','ad','kind'}], sirali ve tekrarsiz.
Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez.""" Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez."""
tanimli = {a.lower() for a in tanimli if a} turler = turler or {}
secilen, gorulen = [], set() secilen, gorulen = [], set()
for m in data: for m in data:
mid = m.get("id") or "" mid = m.get("id") or ""
@@ -125,14 +135,56 @@ def sec(data, tanimli, kapsam="ide"):
if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]): if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]):
continue continue
gorunen = ts.get("display_name") or mid gorunen = ts.get("display_name") or mid
kisa = gorunen.split(" · ")[-1].strip() kind = turler.get(mid) or ts.get("kind") or "model"
combo = "/" not in kisa # saglayici/model bicimi degil: adlandirilmis combo ya da takma ad # Tek uye yolu: 9Router -> OmniRoute -> TurkServis -> Codex.
if kapsam != "hepsi" and not combo and kisa.lower() not in tanimli and mid.lower() not in tanimli: # Dogrudan 9Router satirlari ayni modelin ikinci kopyasi olarak eklenmez.
if ts.get("source") != "omniroute":
continue
# Combo adi biciminden tahmin edilmez; duz adli Agnes/Bekci modelleri de vardir.
if kind == "combo" and mid.lower().startswith("9router-"):
continue continue
gorulen.add(mid) 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"])) secilen.sort(key=lambda s: (s["sira"], s["id"]))
return [{"id": s["id"], "ad": s["ad"]} for s in secilen] # Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
for s in secilen:
taban = re.sub(r" \(\d+\)$", "", s["ad"])
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]
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): def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
@@ -146,5 +198,19 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
return "anahtar-yok", "", "", [] return "anahtar-yok", "", "", []
base, varsayilan, adlar = ide_yapilandirma() base, varsayilan, adlar = ide_yapilandirma()
req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua}) 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", []) with urllib.request.urlopen(req, timeout=timeout) as resp:
return "tamam", base, anahtar, sec(data, adlar + [varsayilan], ayar["kapsam"]) 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
@@ -2,8 +2,8 @@ import json, os, tempfile, unittest
import ide_catalog as c import ide_catalog as c
def girdi(mid, ad, yetenek=("chat",), sira=0): def girdi(mid, ad, yetenek=("chat",), sira=0, kaynak="omniroute", kind="model"):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira}} return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira, "source": kaynak, "kind": kind}}
KATALOG = [ KATALOG = [
@@ -11,8 +11,8 @@ KATALOG = [
girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2), girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2),
girdi("9router-nano-x", "OmniRoute · nano/x", sira=1), girdi("9router-nano-x", "OmniRoute · nano/x", sira=1),
girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)), girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9), girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9, kind="combo"),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8), girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8, kind="combo"),
girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"), girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"),
] ]
@@ -52,14 +52,39 @@ class Yapilandirma(unittest.TestCase):
class Secim(unittest.TestCase): class Secim(unittest.TestCase):
def test_ide_kapsami(self): def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide") s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Tanimli model + adlandirilmis combolar; tanimsiz saglayici modeli ve gorsel modeli yok. # Uye katalogu yerel config'teki adlarla sinirlanmaz.
self.assertEqual([x["id"] for x in s], ["9router-cc-opus", "Ajan-Arac", "9router-Takim-Agir-6e65dba5", "9router-Takim-Agir"]) 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, 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. Dogrudan 9Router tekilleri de eklenmez.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("Takim-Agir", "Takim-Agir"), ("Ajan-Arac", "Ajan-Arac")])
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),
girdi("vag-z", "vag/z", sira=4),
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")
# Yerel model tanimi gerekmez; iki kaynak yerine yalniz OmniRoute yolu gelir.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("9router-nano-x", "nano/x"), ("vag-z", "vag/z")])
def test_gorunen_ad_yalniz_saglayici_ve_model(self): def test_gorunen_ad_yalniz_saglayici_ve_model(self):
for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"), for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"),
("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"), ("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"),
("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"), ("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"),
("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"), ("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"),
("9R · nano/qwen/qwen3.8-27b", "9R · nano/qwen3.8-27b"), ("9R · ag/gemini", "9R · ag/gemini"),
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]: ("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
self.assertEqual(c.kisa_ad(tam), beklenen, tam) self.assertEqual(c.kisa_ad(tam), beklenen, tam)
@@ -74,7 +99,7 @@ class Secim(unittest.TestCase):
def test_hepsi_kapsami_yalniz_sohbet(self): def test_hepsi_kapsami_yalniz_sohbet(self):
s = c.sec(KATALOG, [], "hepsi") s = c.sec(KATALOG, [], "hepsi")
self.assertEqual(len(s), 5) self.assertEqual(len(s), 3) # iki 9Router combosu hicbir kapsamda gosterilmez
self.assertNotIn("gorsel", [x["id"] for x in s]) self.assertNotIn("gorsel", [x["id"] for x in s])
@@ -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()
+1 -2
View File
@@ -27,10 +27,9 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE 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 // 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. // another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '2' 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_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_BACKUP_SUFFIX = '.aiturk-yedek'
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
export const CODEX_LINK_RUN_VALUE = 'AITURK-IDE-Codex-Baglama' export const CODEX_LINK_RUN_VALUE = 'AITURK-IDE-Codex-Baglama'
export const CODEX_LINK_LAUNCHER_NAME = 'baslat.pyw' export const CODEX_LINK_LAUNCHER_NAME = 'baslat.pyw'
+3 -21
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link' import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link'
import { import {
CODEX_BRIDGE_VERSION,
CODEX_LINK_LINE, CODEX_LINK_LINE,
CODEX_LINK_REFRESH_URL, CODEX_LINK_REFRESH_URL,
CODEX_LINK_STATUS_URL, CODEX_LINK_STATUS_URL,
@@ -21,8 +22,7 @@ const PATHS: CodexLinkPaths = {
} }
const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json` const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json`
const BACKUP = `${PATHS.codexConfig}.aiturk-yedek` const STATUS_OK = { ok: true, surum: CODEX_BRIDGE_VERSION, ide_durum: 'tamam', ide_model: 7, ozet: true }
const STATUS_OK = { ok: true, surum: '2', ide_durum: 'tamam', ide_model: 7, ozet: true }
const USER_CONFIG = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n' const USER_CONFIG = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n'
interface World { interface World {
@@ -73,12 +73,6 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
world.events.push(`write ${file}`) world.events.push(`write ${file}`)
world.files.set(file, text) world.files.set(file, text)
}, },
backupOnce: (source, target) => {
if (!world.files.has(target)) {
world.events.push(`backup ${target}`)
world.files.set(target, world.files.get(source) ?? '')
}
},
spawnBridge: (command, args, env) => { spawnBridge: (command, args, env) => {
world.events.push(`spawn ${command} ${JSON.stringify(args)} ${JSON.stringify(env)}`) world.events.push(`spawn ${command} ${JSON.stringify(args)} ${JSON.stringify(env)}`)
spawned = true spawned = true
@@ -212,19 +206,17 @@ describe('applyCodexLink', () => {
hermesHome: PATHS.hermesHome hermesHome: PATHS.hermesHome
}) })
// bridge.json → start → verified answer → config.toml (backed up) → login item // bridge.json → start → verified answer → config.toml → login item
expect(world.events).toEqual([ expect(world.events).toEqual([
`write ${BRIDGE_JSON}`, `write ${BRIDGE_JSON}`,
`spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`, `spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`,
'status ok', 'status ok',
`backup ${BACKUP}`,
`write ${PATHS.codexConfig}`, `write ${PATHS.codexConfig}`,
'autostart set' 'autostart set'
]) ])
expect(JSON.parse(world.files.get(BRIDGE_JSON) ?? '')).toEqual(codexBridgeConfig()) expect(JSON.parse(world.files.get(BRIDGE_JSON) ?? '')).toEqual(codexBridgeConfig())
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`) expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
expect(world.files.get(BACKUP)).toBe(USER_CONFIG)
expect(world.autostart).toBe( expect(world.autostart).toBe(
`"${PATHS.pythonw}" "${PATHS.bridgeScript}" --home "${PATHS.bridgeHome}" --hermes-home "${PATHS.hermesHome}"` `"${PATHS.pythonw}" "${PATHS.bridgeScript}" --home "${PATHS.bridgeHome}" --hermes-home "${PATHS.hermesHome}"`
) )
@@ -238,7 +230,6 @@ describe('applyCodexLink', () => {
expect(status).toMatchObject({ enabled: true, bridgeRunning: false, lastError: 'bridge-start-failed' }) expect(status).toMatchObject({ enabled: true, bridgeRunning: false, lastError: 'bridge-start-failed' })
expect(spawns(world)).toHaveLength(1) expect(spawns(world)).toHaveLength(1)
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG) expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.files.has(BACKUP)).toBe(false)
expect(world.autostart).toBeNull() expect(world.autostart).toBeNull()
expect(writes(world)).toEqual([`write ${BRIDGE_JSON}`]) expect(writes(world)).toEqual([`write ${BRIDGE_JSON}`])
// The start poll is bounded by its one budget. // The start poll is bounded by its one budget.
@@ -289,15 +280,6 @@ describe('applyCodexLink', () => {
expect(spawns(world)).toEqual([]) expect(spawns(world)).toEqual([])
}) })
it('never overwrites the first backup', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(BACKUP, 'the original original')
await link.apply()
expect(world.files.get(BACKUP)).toBe('the original original')
})
it('falls back to python.exe when pythonw.exe is missing', async () => { it('falls back to python.exe when pythonw.exe is missing', async () => {
const { link, world } = createWorld() const { link, world } = createWorld()
world.files.delete(PATHS.pythonw) world.files.delete(PATHS.pythonw)
-9
View File
@@ -23,7 +23,6 @@ import path from 'node:path'
import { import {
addCodexLinkLine, addCodexLinkLine,
CODEX_BRIDGE_VERSION, CODEX_BRIDGE_VERSION,
CODEX_LINK_BACKUP_SUFFIX,
CODEX_LINK_LAUNCHER_NAME, CODEX_LINK_LAUNCHER_NAME,
CODEX_LINK_REFRESH_URL, CODEX_LINK_REFRESH_URL,
CODEX_LINK_RUN_KEY, CODEX_LINK_RUN_KEY,
@@ -67,8 +66,6 @@ export interface CodexLinkDeps {
readText: (file: string) => null | string readText: (file: string) => null | string
/** tmp + rename; creates the parent folder. */ /** tmp + rename; creates the parent folder. */
writeTextAtomic: (file: string, text: string) => void writeTextAtomic: (file: string, text: string) => void
/** Copy `source` to `target` unless `target` already exists. */
backupOnce: (source: string, target: string) => void
/** Start the bridge detached and hidden; it must outlive the IDE. */ /** Start the bridge detached and hidden; it must outlive the IDE. */
spawnBridge: (command: string, args: string[], env: Record<string, string>) => void spawnBridge: (command: string, args: string[], env: Record<string, string>) => void
/** Parsed JSON body, or null when nothing answered within `budgetMs`. */ /** Parsed JSON body, or null when nothing answered within `budgetMs`. */
@@ -334,7 +331,6 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
if (edit.changed) { if (edit.changed) {
try { try {
deps.backupOnce(paths.codexConfig, `${paths.codexConfig}${CODEX_LINK_BACKUP_SUFFIX}`)
deps.writeTextAtomic(paths.codexConfig, edit.text) deps.writeTextAtomic(paths.codexConfig, edit.text)
} catch (error) { } catch (error) {
fail('config-write-failed', error) fail('config-write-failed', error)
@@ -472,11 +468,6 @@ export function createNodeCodexLinkDeps(paths: CodexLinkPaths, log: (message: st
exists: file => fs.existsSync(file), exists: file => fs.existsSync(file),
readText: readTextLossless, readText: readTextLossless,
writeTextAtomic, writeTextAtomic,
backupOnce: (source, target) => {
if (!fs.existsSync(target)) {
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL)
}
},
spawnBridge: (command, args, env) => { spawnBridge: (command, args, env) => {
fs.mkdirSync(paths.bridgeHome, { recursive: true }) fs.mkdirSync(paths.bridgeHome, { recursive: true })
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide", "name": "aiturk-ide",
"productName": "AITURK IDE", "productName": "AITURK IDE",
"private": true, "private": true,
"version": "1.0.0-beta.21", "version": "1.0.0-beta.24",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis", "author": "AITURK / TurkServis",
"repository": { "repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
}, },
"apps/desktop": { "apps/desktop": {
"name": "aiturk-ide", "name": "aiturk-ide",
"version": "1.0.0-beta.21", "version": "1.0.0-beta.24",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",