Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/model_gecis.py
T
yilsemandClaude Opus 5.5 8a612beba7
ci / mobile-link-windows (push) Canceled after 0s
ci / approval-gate-windows (push) Canceled after 0s
feat(codex-bridge): tell the model the chat model can be changed by saying so; a nameless command asks which
Measured in a live voice chat: asked whether it could change the model, Opus answered "I cannot change this
chat's model here". Every request now carries one short developer note, and "modeli değiştir" without a
name is answered by the bridge with how to say it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:55:34 +03:00

347 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
YENILE_SN = 24 * 3600
EN_COK_KAYIT = 200
EN_COK_KELIME = 14
_KILIT = threading.Lock()
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
# Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir.
FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim",
"degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim",
"cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"}
OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme")
# "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir.
SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana",
"modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"}
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", "nokta"}
# Yaziyla soylenen surum rakamlari ("opus bes bes", "uc nokta sekiz"). "bir" belirtec de olabildigi icin
# ("bir model sec") yalniz baska bir rakamin ya da "nokta"nin yaninda rakam sayilir.
SAYI = {"sifir": "0", "bir": "1", "iki": "2", "uc": "3", "dort": "4", "bes": "5", "alti": "6", "yedi": "7",
"sekiz": "8", "dokuz": "9"}
# Komutun ardindan gelen ve is tasimayan ek ("... yap ve devam et"): komutu dusurmez, atilir.
DEVAM_EKI = {"ve", "sonra", "devam", "et", "edelim", "edin", "lutfen"}
# 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 _sayilari_cevir(kelimeler):
cikti = []
for i, k in enumerate(kelimeler):
if k in SAYI:
komsu = [kelimeler[j] for j in (i - 1, i + 1) if 0 <= j < len(kelimeler)]
if k != "bir" or any(x == "nokta" or x.isdigit() or (x in SAYI and x != "bir") for x in komsu):
cikti.append(SAYI[k])
continue
cikti.append(k)
return cikti
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"} if any(k in SORU_ISARETI for k in kelimeler) else None
if any(k.startswith(OLUMSUZ) for k in kelimeler):
return None
fiil = [k for k in kelimeler if k in FIILLER]
if fiil:
son_fiil = max(i for i, k in enumerate(kelimeler) if k in FIILLER)
if all(k in DEVAM_EKI for k in kelimeler[son_fiil + 1:]):
kelimeler = kelimeler[:son_fiil + 1]
kelimeler = _sayilari_cevir(kelimeler)
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)]
# Ad soylenmemis ("modeli değiştir", "başka modele geç"): degisiklik yok, nasil soylenecegi anlatilir.
return {"tur": "gec", "ad": ad} if ad else {"tur": "nasil"}
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
kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
if not puanli:
return None
en = max(p[0] for p in puanli)
harfler = [s for s in ad if not s.isdigit()]
en_harf = max(p[4] for p in puanli)
if en_harf < len(harfler):
return None # adin bir sozcugu hicbir modelde yok: komut degil
iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3]))
if not iyi:
return None
if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme
return {"adaylar": [p[3] for p in iyi[:4]]}
if len(iyi) == 1:
return {"slug": iyi[0][2], "ad": iyi[0][3]}
tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model
if tam:
# Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan,
# sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur.
def oncelik(p):
return (p[2].startswith("ide/"), -p[5], "/" not in p[3])
tam.sort(key=oncelik)
if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]):
return {"slug": tam[0][2], "ad": tam[0][3]}
return {"adaylar": [p[3] for p in tam[:4]]}
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, etkin=None):
"""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).
etkin: sohbetin su an gercekten kullandigi model. Verilirse, en son kullanici mesajindaki komut (ardindan
arac ciktisi ya da sikistirma tetigi geldigi icin "gecmis" gorunen) yalniz bu modele gecirdiyse nota cevrilir;
uygulanmamis komut ("baglanti kapali" denen gibi) "degistirildi" diye anlatilmaz.
Doner: degistirilen mesaj sayisi."""
girdi = req.get("input")
if not isinstance(girdi, list):
return 0
son = son_sira(girdi)
son_kullanici = max((i for i, oge in enumerate(girdi) if isinstance(oge, dict)
and oge.get("type", "message") == "message" and oge.get("role") == "user"), default=-1)
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:
if etkin is not None and i == son_kullanici and sonuc["slug"] != etkin:
continue
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
BILGI_ISARETI = "[Model değiştirme]"
BILGI = (BILGI_ISARETI + " Bu sohbetin modeli söylenerek değiştirilebilir: kullanıcı \"modeli <ad> yap\" dediğinde "
"(örnek: \"modeli gemini flash low yap\", \"modeli opus beş beş yap\") değişikliği aradaki köprü yapar; senin bir "
"araç çağırman ya da ayar değiştirmen gerekmez. Kullanıcı modeli değiştirip değiştiremeyeceğini sorarsa "
"\"değiştiremem\" deme; bu cümleyi söylemesinin yeterli olduğunu kısaca anlat. Hangi modelde olduğunu öğrenmek "
"için \"seçili model hangisi\" demesi yeter.")
NASIL = ("Hangi modele geçeyim? Adını söylemen yeterli, örneğin: \"modeli gemini flash low yap\" ya da "
"\"modeli opus beş beş yap\". Şu anki modeli öğrenmek için \"seçili model hangisi\" de.")
def bilgi_ekle(req):
"""Modele, sohbetin modelinin soylenerek degistirilebildigini bildirir (olcum 11 Ekim 2026: Opus "bu sohbetin
modelini buradan değiştiremem" dedi). Istegin basina tek developer mesaji; zaten varsa eklenmez. True: eklendi."""
girdi = req.get("input")
if not isinstance(girdi, list) or not girdi:
return False
for oge in girdi[:4]:
icerik = oge.get("content") if isinstance(oge, dict) else None
yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
if BILGI_ISARETI in yazi:
return False
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": BILGI}]})
return True
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
if simdi - float(k.get("zaman") or 0) > YENILE_SN:
# Kullanilan yonlendirme eskimez: omur son kullanimdan sayilir (gunde en cok bir yazma).
k["zaman"] = simdi
self._yaz()
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()