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>
347 lines
17 KiB
Python
347 lines
17 KiB
Python
"""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()
|