217 lines
9.4 KiB
Python
217 lines
9.4 KiB
Python
"""AITURK IDE "Codex Baglama": IDE'nin model ve combolarini Codex listesine ekler.
|
|
|
|
Kaynak, IDE'nin kullandigi gecittir (config.yaml -> providers.turkservis.base_url);
|
|
anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazilmaz.
|
|
|
|
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" ve "hepsi": gecidin uye katalogundaki OmniRoute sohbet modelleri.
|
|
Yerel config'teki model adlari uye katalogunu sinirlamaz.
|
|
"""
|
|
import json, os, re, urllib.request
|
|
|
|
# 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))
|
|
AYAR = os.path.join(IDE_DIR, "codex-baglama.json")
|
|
KEY_NAME = "HERMES_CUSTOM_TURKSERVIS_API_KEY"
|
|
DEFAULT_BASE = "https://ai.turkservis.online/v1"
|
|
|
|
|
|
def ayar_oku(yol=AYAR):
|
|
"""Dosya yoksa ya da bozuksa varsayilan: acik, kapsam 'ide'."""
|
|
ayar = {"enabled": True, "kapsam": "ide"}
|
|
try:
|
|
with open(yol, encoding="utf-8") as f:
|
|
v = json.load(f)
|
|
if isinstance(v, dict):
|
|
if isinstance(v.get("enabled"), bool):
|
|
ayar["enabled"] = v["enabled"]
|
|
if v.get("kapsam") in ("ide", "hepsi"):
|
|
ayar["kapsam"] = v["kapsam"]
|
|
except (OSError, ValueError):
|
|
pass
|
|
return ayar
|
|
|
|
|
|
def anahtar_oku(agent=AGENT):
|
|
try:
|
|
with open(os.path.join(agent, ".env"), encoding="utf-8", errors="replace") as f:
|
|
for line in f:
|
|
if line.startswith(KEY_NAME + "="):
|
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
except OSError:
|
|
pass
|
|
return ""
|
|
|
|
|
|
def ide_yapilandirma(agent=AGENT):
|
|
"""config.yaml'dan gecit adresi, varsayilan model ve tanimli model adlari.
|
|
Kopru bagimliliksiz calistigi icin YAML'in yalniz bu bolumu satir satir okunur."""
|
|
base, varsayilan, adlar = DEFAULT_BASE, "", []
|
|
try:
|
|
with open(os.path.join(agent, "config.yaml"), encoding="utf-8", errors="replace") as f:
|
|
lines = f.read().splitlines()
|
|
except OSError:
|
|
return base, varsayilan, adlar
|
|
bolum = saglayici = None
|
|
modellerde = False
|
|
for line in lines:
|
|
if not line.strip() or line.lstrip().startswith("#"):
|
|
continue
|
|
girinti = len(line) - len(line.lstrip(" "))
|
|
metin = line.strip()
|
|
if girinti == 0:
|
|
bolum = metin.rstrip(":"); saglayici = None; modellerde = False
|
|
continue
|
|
if bolum == "model" and girinti == 2:
|
|
if metin.startswith("default:"):
|
|
varsayilan = metin.split(":", 1)[1].strip().strip("'\"")
|
|
continue
|
|
if bolum != "providers":
|
|
continue
|
|
if girinti == 2:
|
|
saglayici = metin.rstrip(":"); modellerde = False
|
|
elif saglayici == "turkservis" and girinti == 4:
|
|
modellerde = metin == "models:"
|
|
if metin.startswith("base_url:"):
|
|
base = metin.split(":", 1)[1].strip().strip("'\"") or base
|
|
elif saglayici == "turkservis" and modellerde and girinti == 6:
|
|
ad = re.split(r":\s*(\{|$)", metin)[0].strip().strip("'\"")
|
|
if ad:
|
|
adlar.append(ad)
|
|
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):
|
|
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
|
|
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
|
|
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."""
|
|
on = _isaret(ad)
|
|
ad = (ad or "").split(" · ")[-1].strip()
|
|
parcalar = [p for p in ad.split("/") if p]
|
|
if len(parcalar) <= 2:
|
|
return on + ("/".join(parcalar) or ad)
|
|
return on + parcalar[0] + "/" + parcalar[-1]
|
|
|
|
|
|
def adlandir(girdiler, dolu=()):
|
|
"""[(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."""
|
|
kisa = {k: kisa_ad(a) for k, a in girdiler}
|
|
tam = {k: _isaret(a) + (a or "").split(" · ")[-1].strip() for k, a in girdiler}
|
|
sayac = {}
|
|
for v in list(kisa.values()) + list(dolu):
|
|
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
|
|
sonuc, kullanilan = {}, {d.lower() for d in dolu}
|
|
for k, _ in girdiler:
|
|
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
|
|
if ad.lower() in kullanilan:
|
|
ad = f"{ad} [{k[-8:]}]"
|
|
kullanilan.add(ad.lower())
|
|
sonuc[k] = ad
|
|
return sonuc
|
|
|
|
|
|
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."""
|
|
turler = turler or {}
|
|
secilen, gorulen = [], set()
|
|
for m in data:
|
|
mid = m.get("id") or ""
|
|
ts = m.get("turkservis") or {}
|
|
if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]):
|
|
continue
|
|
gorunen = ts.get("display_name") or mid
|
|
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
|
|
# 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, "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}
|
|
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):
|
|
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
|
|
Ag hatasinda istisna atar; cagiran son listeyi korur."""
|
|
ayar = ayar_oku()
|
|
if not ayar["enabled"]:
|
|
return "kapali", "", "", []
|
|
anahtar = anahtar_oku()
|
|
if not anahtar:
|
|
return "anahtar-yok", "", "", []
|
|
base, varsayilan, adlar = ide_yapilandirma()
|
|
req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
|
|
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
|