feat(codex-bridge): change the chat model by saying so; bridge 8, IDE 1.0.0-beta.27

A model picked on the phone only arrives with a typed message that starts a turn. The bridge now
handles "modeli X yap" itself: per-chat routing, same-turn handover when a model is working,
truthful answer to "which model", and a lone "." joining a running turn no longer cuts the work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 00:05:18 +03:00
1 parent acf88f8314
commit 959f5f1459
7 files changed
+495 -15

No files matched your search

@@ -27,6 +27,24 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory.
## Version 8 (IDE beta.27): changing the model by saying so
Measured on a live voice chat (11 Oct 2026): a model picked on the phone reaches this machine only with a typed
message that STARTS a turn; a message that joins a running turn (including the lone `.`) does not carry it.
`model_gecis.py` therefore handles the change in the bridge. A short user message such as "modeli gemini flash low
yap" (spoken through the voice assistant or typed) is matched against the model list; the chat's requests are then
routed to that model (`model-yonlendirme.json`, keyed by `prompt_cache_key`) until Codex sends a different model
for the chat, i.e. until the user picks another one in the list.
- Idle chat: the bridge answers itself ("Sohbetin modeli artık X ..."), which also tells the voice assistant.
- Running turn: the command is replaced by a handover note and the SAME turn continues on the new model.
- "seçili model hangisi" / "ana model kim" is answered by the bridge with the model actually in use.
- An ambiguous name changes nothing and lists the closest names; a name matching no model is not a command.
- A lone `.` that joins a running turn is dropped instead of answered (answering it cut the work short).
- Handled commands left in the history are turned into notes so a later model does not answer them again.
Setting: `sesle_model` (default on).
## 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).
+109 -12
View File
@@ -27,8 +27,9 @@ import ide_catalog
import ozet_kopru
import pencere
import yarim
import model_gecis
SURUM = "7"
SURUM = "8"
HERE = os.path.dirname(os.path.abspath(__file__))
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
@@ -583,7 +584,12 @@ def nokta_mesaji_mi(req):
girdi = req.get("input")
if not isinstance(girdi, list) or not girdi:
return False
son = girdi[-1]
son = None
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
continue
son = oge
break
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return False
icerik = son.get("content")
@@ -651,6 +657,28 @@ def decompress(body, enc):
return body
OZET_FRENI = ozet_kopru.DonguFreni()
YONLENDIRME = model_gecis.Yonlendirme(HOME)
_BILINEN = {"kaynak": None, "liste": []}
def bilinen_modeller():
"""Codex'e en son verilen listedeki (slug, gorunen ad) ciftleri: abonelik modelleri + eklenenler."""
son = max(LAST_MODELS.values(), key=lambda v: str(v.get("at") or ""), default=None) if LAST_MODELS else None
govde = son.get("body") if son else None
if govde is not None and _BILINEN["kaynak"] is not govde:
try:
liste = [(m["slug"], m.get("display_name") or m["slug"]) for m in json.loads(govde).get("models", [])
if isinstance(m, dict) and m.get("slug")]
except (ValueError, AttributeError, TypeError):
liste = []
_BILINEN["kaynak"], _BILINEN["liste"] = govde, liste
liste = list(_BILINEN["liste"])
gorulen = {slug for slug, _ in liste}
liste += [(m["slug"], m.get("display_name") or m["slug"]) for m in EXTRA_MODELS
if m.get("slug") and m["slug"] not in gorulen]
return liste
def model_adi(slug):
return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@@ -982,6 +1010,54 @@ class H(http.server.BaseHTTPRequestHandler):
self.wfile.write(out)
log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi")
def _yerel_yanit(self, slug, metin):
govde = hazir_yaniti(slug, metin)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
def _model_komutu(self, n, req, sohbet, gelen):
"""Son kullanici mesaji "modeli X yap" / "hangi model" ise isler. Yerel cevap metni doner;
None: istek (gerekirse degistirilmis olarak) modele gider."""
metin, sira, sesli = model_gecis.son_kullanici_metni(req)
komut = model_gecis.komut_coz(metin) if metin else None
if not komut:
return None
etkin = YONLENDIRME.al(sohbet, gelen) or gelen
ortada = model_gecis.tur_ortasi(req, sira)
if komut["tur"] == "sor":
if ortada:
return None
log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}")
return f"Sohbetin modeli şu an {model_adi(etkin)}."
sonuc = model_gecis.eslestir(komut["ad"], bilinen_modeller())
if not sonuc:
return None # ad hicbir modele benzemiyor: komut degil, modele gider
if "adaylar" in sonuc:
soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: "
+ ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?")
log(f"[{n} {gelen}] model komutu belirsiz: {komut['ad']} -> {sonuc['adaylar']}")
if not ortada:
return soru
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; "
"model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]")
return None
hedef = sonuc["slug"]
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
return "AITURK bağlantısı şu an kapalı; bu modele geçemedim, model değişmedi."
YONLENDIRME.koy(sohbet, gelen, hedef)
log(f"[{n} {gelen}] model komutu: {model_adi(etkin)} -> {sonuc['ad']}"
f" ({'tur ortasi, is devrediliyor' if ortada else 'bosta'}{', sesli' if sesli else ''})")
if hedef == etkin:
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
if ortada:
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
return None
return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. "
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.")
def _responses(self, n, path, body):
try:
req = json.loads(decompress(body, self.headers.get("Content-Encoding")))
@@ -989,21 +1065,42 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} responses] govde cozulemedi: {e}")
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
slug = req.get("model", "")
gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model)
govde_degisti = False # istek govdesi kopruce degistirildi: abonelik yolunda yeniden yazilir
once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)
sohbet = str(req.get("prompt_cache_key") or "")
if CFG.get("sesle_model", True) and sohbet:
try:
yerel = self._model_komutu(n, req, sohbet, gelen)
except Exception as e:
yerel = None
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
if yerel is not None:
return self._yerel_yanit(gelen, yerel)
try:
if model_gecis.gecmisi_temizle(req, bilinen_modeller()):
govde_degisti = True
except Exception as e:
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
hedef = YONLENDIRME.al(sohbet, gelen)
if hedef and hedef != gelen:
req["model"] = slug = hedef
log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor")
mapping = MODEL_MAP.get(slug)
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
del req["input"][model_gecis.son_sira(req["input"]):]
govde_degisti = True
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
govde_degisti = True
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req):
# Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da
# istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur.
ad = next((m.get("display_name") for m in EXTRA_MODELS if m.get("slug") == slug), None) or slug
govde = hazir_yaniti(slug, f"{ad} hazır.")
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
return
return self._yerel_yanit(gelen, f"{model_adi(slug)} hazır. Sohbetin modeli artık {model_adi(slug)}.")
# Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir.
sohbet = str(req.get("prompt_cache_key") or "")
yarim_eklenen = 0
if CFG.get("yarim_cevap", True):
try:
@@ -1037,7 +1134,7 @@ class H(http.server.BaseHTTPRequestHandler):
# abonelik modeli: chatgpt.com'a oldugu gibi
fixed = fix_tool_ids(req)
atilan = drop_foreign_items(req)
degisti = bool(yarim_eklenen)
degisti = bool(yarim_eklenen) or govde_degisti
ozet_akisi = None
if ozet_acik:
# Ozet bagdastirici (ozet_kopru.py; 9 Ekim 2026'da yalniz bu parca geri acildi): koprunun urettigi
@@ -0,0 +1,259 @@
"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026).
Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla
ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu.
Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik
koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir),
kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir.
- Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ...").
- Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer;
yarim kalan is kesilmez, yeni model kaldigi yerden devam eder.
- Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar.
Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur.
"""
import json
import os
import re
import threading
import time
DOSYA = "model-yonlendirme.json"
OMUR_SN = 7 * 24 * 3600
EN_COK_KAYIT = 200
EN_COK_KELIME = 14
_KILIT = threading.Lock()
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
SORU = ("hangi", "hangisi", "kim", "kimdir")
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
"bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex",
"ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona"}
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
"kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex",
"dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku",
"sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking",
"ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"}
# Listede kisa yazilan saglayici -> konusurken soylenen ad
SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")}
def duz(metin):
return str(metin or "").lower().translate(_TR)
def parcala(metin):
"""Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low."""
return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))]
def son_sira(girdi):
"""Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar
kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1."""
if not isinstance(girdi, list):
return -1
for i in range(len(girdi) - 1, -1, -1):
oge = girdi[i]
if isinstance(oge, dict) and oge.get("type", "message") == "message":
if oge.get("role") == "user":
return i
if oge.get("role") in ("developer", "system"):
continue
return -1
return -1
def son_kullanici_metni(req):
"""(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False).
Sesli aktarimda (<realtime_delegation>) yalnizca <input> icindeki soz alinir."""
girdi = req.get("input")
sira = son_sira(girdi)
if sira < 0:
return None, -1, False
son = girdi[sira]
icerik = son.get("content")
if isinstance(icerik, str):
metin = icerik
elif isinstance(icerik, list) and icerik and all(isinstance(p, dict) and p.get("type") in ("input_text", "text")
for p in icerik):
metin = "".join(str(p.get("text") or "") for p in icerik)
else:
return None, -1, False
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
if m:
return m.group(1).strip(), sira, True
if "<" in metin and ">" in metin:
return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz
return metin.strip(), sira, False
def komut_coz(metin):
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
kelimeler = re.findall(r"[a-z0-9]+", duz(metin))
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
return None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"}
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
if fiil:
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
return {"tur": "gec", "ad": ad} if ad else None
return None
def _uyar(sorgu, parca):
"""Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar."""
if sorgu == parca:
return True
return len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit()
def eslestir(ad, modeller):
"""ad: sorgu parcalari; modeller: [(slug, gorunen ad)].
Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor)."""
if not ad:
return None
puanli = []
for slug, gorunen in modeller:
parcalar = parcala(gorunen) or parcala(slug)
kume = set(parcalar) | set(parcala(slug))
for kisa, uzunlar in SAGLAYICI.items():
if kisa in kume:
kume.update(uzunlar)
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
puanli.append((tutan, -len(parcalar), slug, gorunen))
if not puanli:
return None
en = max(p[0] for p in puanli)
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
return None # sozun cogu hicbir modele uymuyor: komut degil
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
return {"slug": iyi[0][2], "ad": iyi[0][3]}
return {"adaylar": [p[3] for p in iyi[:4]]}
_ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning",
"local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"}
def tur_ortasi(req, sira):
"""Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir:
once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir."""
girdi = req.get("input") or []
for oge in reversed(girdi[:max(sira, 0)]):
if not isinstance(oge, dict):
continue
tur = oge.get("type", "message")
if tur in _ARAC:
return True
if tur == "message":
if oge.get("role") == "assistant":
return oge.get("phase") == "commentary"
continue
return False
def metni_degistir(req, sira, yeni):
"""sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca <input> icini)."""
oge = req["input"][sira]
icerik = oge.get("content")
eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik)
if "<realtime_delegation>" in eski:
metin = re.sub(r"(<input>).*?(</input>)", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S)
else:
metin = yeni
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
def gecmisi_temizle(req, modeller):
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026).
Doner: degistirilen mesaj sayisi."""
girdi = req.get("input")
if not isinstance(girdi, list):
return 0
son = son_sira(girdi)
n = 0
for i, oge in enumerate(girdi):
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
continue
metin, _, _ = son_kullanici_metni({"input": [oge]})
if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"):
continue
komut = komut_coz(metin)
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
if sonuc and "slug" in sonuc:
metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; "
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
n += 1
return n
def devir_notu(yeni_ad, eski_ad):
return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren "
f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]")
class Yonlendirme:
"""sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir."""
def __init__(self, home):
self.yol = os.path.join(home, DOSYA) if home else None
self.veri = {}
if self.yol:
try:
with open(self.yol, encoding="utf-8") as f:
okunan = json.load(f)
if isinstance(okunan, dict):
self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")}
except (OSError, ValueError):
pass
def _yaz(self):
if not self.yol:
return
try:
gecici = self.yol + ".tmp"
with open(gecici, "w", encoding="utf-8") as f:
json.dump(self.veri, f, ensure_ascii=False)
os.replace(gecici, self.yol)
except OSError:
pass
def al(self, sohbet, gelen, simdi=None):
"""Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni
model secti) ya da kayit eskidiyse yonlendirme silinir."""
simdi = simdi or time.time()
with _KILIT:
k = self.veri.get(sohbet)
if not k:
return None
if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN:
del self.veri[sohbet]
self._yaz()
return None
return k["hedef"]
def koy(self, sohbet, gelen, hedef, simdi=None):
simdi = simdi or time.time()
with _KILIT:
if hedef == gelen:
self.veri.pop(sohbet, None)
else:
self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi}
if len(self.veri) > EN_COK_KAYIT:
for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]:
del self.veri[eski]
self._yaz()
@@ -0,0 +1,106 @@
"""Bridge 8: "modeli X yap" is handled by the bridge; a per-chat route survives until the user picks another model."""
import tempfile
import unittest
import model_gecis as g
MODELS = [("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("gpt-6.1-sol", "GPT-6.1-Sol"),
("ide/9router-ag-gemini-3.8-flash-low", "agy/gemini-3.8-flash-low"),
("ide/9router-ag-gemini-3.8-flash-high", "agy/gemini-3.8-flash-high"),
("ide/9router-cc-claude-opus-5-5", "cc/opus-5-5"), ("ide/9router-cc-claude-opus-5-5-nt", "cc/opus-5-5 (no-think)"),
("ide/9router-nano-z-ai-glm-5.3-flash-uncensored", "nano/glm-5.3-flash-uncensored"),
("ide/agnes-agnes-2.0-flash", "agnes/agnes-2.0-flash"), ("ide/agnes-agnes-2.5-flash", "agnes/agnes-2.5-flash")]
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
def voice(text):
return user(f"<realtime_delegation>\n <input>{text}</input>\n <transcript_delta>user: x</transcript_delta>\n</realtime_delegation>")
def pick(text):
command = g.komut_coz(text)
return g.eslestir(command["ad"], MODELS) if command and command["tur"] == "gec" else command
class Komut(unittest.TestCase):
def test_spoken_names_resolve_to_one_model(self):
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
("modeli klod opus yap", "ide/9router-cc-claude-opus-5-5"),
("modeli opusa geç", "ide/9router-cc-claude-opus-5-5"),
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]:
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
self.assertEqual(pick("modeli agnes yap"), {"adaylar": ["agnes/agnes-2.0-flash", "agnes/agnes-2.5-flash"]})
def test_ordinary_sentences_are_not_commands(self):
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
"modeli değiştir", "hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
"model " + "çok uzun bir cümle " * 5 + "yap"):
self.assertFalse(pick(text) and "slug" in pick(text), text)
self.assertEqual(g.komut_coz("seçili model hangisi"), {"tur": "sor"})
self.assertEqual(g.komut_coz("bu chatte ana model kim"), {"tur": "sor"})
def test_voice_delegation_yields_only_the_spoken_input(self):
text, index, spoken = g.son_kullanici_metni({"input": [user("a"), voice("modeli opus yap")]})
self.assertEqual((text, index, spoken), ("modeli opus yap", 1, True))
self.assertEqual(g.son_kullanici_metni({"input": [user("<skill>x</skill>")]})[0], None)
self.assertEqual(g.son_kullanici_metni({"input": [{"type": "function_call_output", "output": "x"}]})[0], None)
class TurOrtasi(unittest.TestCase):
def test_message_after_tool_items_is_mid_turn_and_after_final_answer_is_not(self):
final = {"type": "message", "role": "assistant", "phase": "final_answer", "content": "ok"}
call = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
out = {"type": "function_call_output", "call_id": "c", "output": "x"}
self.assertFalse(g.tur_ortasi({"input": [user("a"), final, user(".")]}, 2))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, user(".")]}, 3))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, voice("bekliyorum"), user(".")]}, 4))
self.assertTrue(g.tur_ortasi({"input": [user("a"), {"type": "message", "role": "assistant", "phase": "commentary",
"content": "bakıyorum"}, user(".")]}, 2))
self.assertFalse(g.tur_ortasi({"input": [user(".")]}, 0))
def test_handover_note_replaces_only_the_spoken_input(self):
req = {"input": [voice("modeli opus yap")]}
g.metni_degistir(req, 0, g.devir_notu("cc/opus-5-5", "GPT-6-Sol"))
text = req["input"][0]["content"][0]["text"]
self.assertIn("<transcript_delta>", text)
self.assertIn("cc/opus-5-5 olarak devam ediyorum", text)
self.assertNotIn("modeli opus yap</input>", text)
class Gecmis(unittest.TestCase):
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
voice("modeli opus yap"), user("modeli gpt 6 astra yap")]}
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
self.assertIn("<transcript_delta>", req["input"][2]["content"][0]["text"])
self.assertEqual(req["input"][3]["content"][0]["text"], "modeli gpt 6 astra yap")
self.assertEqual(g.gecmisi_temizle(req, MODELS), 0)
class Yon(unittest.TestCase):
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
with tempfile.TemporaryDirectory() as home:
y = g.Yonlendirme(home)
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
self.assertEqual(g.Yonlendirme(home).al("chat", "gpt-6-sol", simdi=1001), "ide/x") # survives a restart
self.assertIsNone(y.al("other", "gpt-6-sol", simdi=1001))
self.assertIsNone(y.al("chat", "gpt-6-astra", simdi=1002)) # user picked another
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1003)) # and it stays cleared
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
y.koy("chat", "gpt-6-sol", "gpt-6-sol", simdi=1001) # back to the picked one
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1002))
y.koy("old", "a", "b", simdi=1000)
self.assertIsNone(y.al("old", "a", simdi=1000 + g.OMUR_SN + 1))
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -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 = '7'
export const CODEX_BRIDGE_VERSION = '8'
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'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.26",
"version": "1.0.0-beta.27",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.26",
"version": "1.0.0-beta.27",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",