From c8993d58cc932be6945760bf304511e919704c60 Mon Sep 17 00:00:00 2001 From: yilsem Date: Sun, 11 Oct 2026 07:25:30 +0300 Subject: [PATCH] feat(codex-bridge): web search for added models, forgiving spoken model names Added models had no web search: Codex's built-in one runs on OpenAI's side and the bridge drops it from their tool list. Measured in a voice session on 11 Oct 2026: a model looked for a search tool ten times and ended the turn with no answer. The bridge now tells added models two ready commands (ara.py: search and read a page, standard library only, no key). Codex's own models never see the note: their requests do not reach the bridge. Spoken model names: a half-spoken word matches the start of a name part ("think" -> thinking, "obli" -> obliterated), and when one candidate is the others minus extra words, the one without the unsaid suffix is picked ("Qwen 3.8 Obliterated" -> not the ":thinking" one). Co-Authored-By: Claude Opus 5.5 --- apps/desktop/assets/codex-bridge/ara.py | 106 ++++++++++++++++++ apps/desktop/assets/codex-bridge/internet.py | 57 ++++++++++ apps/desktop/assets/codex-bridge/kopru.py | 4 + .../assets/codex-bridge/model_gecis.py | 15 ++- .../assets/codex-bridge/test_internet.py | 67 +++++++++++ .../assets/codex-bridge/test_model_gecis.py | 18 ++- 6 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/assets/codex-bridge/ara.py create mode 100644 apps/desktop/assets/codex-bridge/internet.py create mode 100644 apps/desktop/assets/codex-bridge/test_internet.py diff --git a/apps/desktop/assets/codex-bridge/ara.py b/apps/desktop/assets/codex-bridge/ara.py new file mode 100644 index 0000000..60a99bd --- /dev/null +++ b/apps/desktop/assets/codex-bridge/ara.py @@ -0,0 +1,106 @@ +"""Internet aramasi ve sayfa okuma: Codex'e sonradan eklenen modeller icin (11 Ekim 2026). + +Codex'in yerlesik web aramasi OpenAI sunucusunda calisir ve yalniz Codex'in kendi modellerine verilir; kopru onu +ek modele giden arac listesinden atar. Ek model aramayi bu betigi kabuk araciyla calistirarak yapar (internet.py +modele komutu soyler). Anahtar gerekmez; yalniz standart kitaplik. + + python ara.py ara "sorgu" [--adet 8] + python ara.py oku [--en-cok 8000] +""" +import argparse +import html +import re +import sys +import urllib.parse +import urllib.request + +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36" +ARAMA = "https://html.duckduckgo.com/html/?" +SURE = 25 +EN_COK_BAYT = 3_000_000 + + +def indir(adres, sure=SURE): + adres = urllib.parse.quote(adres, safe=":/?#[]@!$&'()*+,;=%~") # Turkce harfli adres ("/wiki/Türkiye") + istek = urllib.request.Request(adres, headers={"User-Agent": UA, "Accept-Language": "tr,en;q=0.8"}) + with urllib.request.urlopen(istek, timeout=sure) as yanit: + ham = yanit.read(EN_COK_BAYT) + kodlama = yanit.headers.get_content_charset() or "utf-8" + tur = yanit.headers.get_content_type() + try: + return ham.decode(kodlama, "replace"), tur + except LookupError: + return ham.decode("utf-8", "replace"), tur + + +def _duz(parca): + return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", "", parca))).strip() + + +def sonuclari_coz(sayfa, adet=8): + """Arama sayfasindan [(baslik, adres, ozet)].""" + cikti = [] + for blok in re.split(r']+class="[^"]*\bresult\b[^"]*"', sayfa)[1:]: + bag = re.search(r']+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)', blok, re.S) + if not bag: + continue + adres = html.unescape(bag.group(1)) + ic = re.search(r"[?&]uddg=([^&]+)", adres) + if ic: + adres = urllib.parse.unquote(ic.group(1)) + if adres.startswith("//"): + adres = "https:" + adres + if "duckduckgo.com/y.js" in adres: # reklam + continue + ozet = re.search(r'class="result__snippet"[^>]*>(.*?)', blok, re.S) + cikti.append((_duz(bag.group(2)), adres, _duz(ozet.group(1)) if ozet else "")) + if len(cikti) >= adet: + break + return cikti + + +def metne_cevir(sayfa): + """HTML -> okunur duz metin (betik, bicim ve gezinme kisimlari atilir).""" + sayfa = re.sub(r"(?is)<(script|style|noscript|svg|nav|footer|header|form|iframe)\b.*?", " ", sayfa) + sayfa = re.sub(r"(?is)", " ", sayfa) + sayfa = re.sub(r"(?i)<(br|/p|/div|/li|/h[1-6]|/tr)\b[^>]*>", "\n", sayfa) + metin = html.unescape(re.sub(r"<[^>]+>", " ", sayfa)) + satirlar = [re.sub(r"[ \t\r\f\v]+", " ", s).strip() for s in metin.split("\n")] + return "\n".join(s for s in satirlar if s) + + +def ara(sorgu, adet=8, indirici=indir): + sayfa, _ = indirici(ARAMA + urllib.parse.urlencode({"q": sorgu})) + sonuc = sonuclari_coz(sayfa, adet) + if not sonuc: + return "Sonuç bulunamadı. Sorguyu daha kısa ya da farklı sözcüklerle yeniden dene." + return "\n\n".join(f"{i}. {b}\n {a}\n {o}" for i, (b, a, o) in enumerate(sonuc, 1)) + + +def oku(adres, en_cok=8000, indirici=indir): + if not re.match(r"https?://", adres): + adres = "https://" + adres + sayfa, tur = indirici(adres) + metin = metne_cevir(sayfa) if "html" in tur or " en_cok: + metin = metin[:en_cok] + f"\n[... kesildi: {len(metin)} karakterin ilk {en_cok} karakteri]" + return metin or "Sayfada okunur metin bulunamadı." + + +def main(argv=None): + p = argparse.ArgumentParser(description="Internet aramasi ve sayfa okuma") + alt = p.add_subparsers(dest="is_", required=True) + a = alt.add_parser("ara"); a.add_argument("sorgu", nargs="+"); a.add_argument("--adet", type=int, default=8) + o = alt.add_parser("oku"); o.add_argument("adres"); o.add_argument("--en-cok", type=int, default=8000) + s = p.parse_args(argv) + try: + metin = ara(" ".join(s.sorgu), max(1, min(s.adet, 20))) if s.is_ == "ara" else oku(s.adres, max(500, s.en_cok)) + kod = 0 + except Exception as e: + metin, kod = f"Hata: {type(e).__name__}: {e}", 1 + sys.stdout.buffer.write((metin + "\n").encode("utf-8", "replace")) + return kod + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/apps/desktop/assets/codex-bridge/internet.py b/apps/desktop/assets/codex-bridge/internet.py new file mode 100644 index 0000000..70c9c77 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/internet.py @@ -0,0 +1,57 @@ +"""Ek modellere internet aramasi (11 Ekim 2026). + +Kullanici karari (11 Ekim 2026, sesli): "Arastirma yapabilmesini istiyorum. Ayni Codex'te yapabildikleri gibi olmasini +istiyorum." Codex'in yerlesik web aramasi OpenAI sunucusunda calistigi icin ek modele verilemez (kopru onu arac +listesinden atar); olcum: ek model arama araci bulamayinca turu bos bitirdi. Kopru bunun yerine ek modele giden +istege, aramayi kabuk araciyla yapan hazir komutu (ara.py) soyleyen kisa bir not ekler. Codex'in kendi modelleri bu +notu hic gormez: onlarin istegi kopruye ugramaz. + +Ayar (bridge.json, istege bagli): "internet": {"enabled": false} kapatir; {"talimat": "..."} metni degistirir. +""" +import os +import sys + +ISARET = "[İnternet araması]" +VARSAYILAN = ( + ISARET + " Bu modelde yerleşik web araması yok; internetten bilgi gerektiğinde (güncel olay, araştırma, kaynak, " + "fiyat, \"araştır\" isteği) kabuk aracınla şu iki komutu kullan:\n" + "Arama: {ara} \"sorgu\"\n" + "Sayfa okuma: {oku} \n" + "Önce ara, sonra işe yarayan iki üç sonucu oku, cevabında kaynak adreslerini belirt. Başka bir arama aracı " + "arama (tool_search ile bulunmaz). Komut hata verirse sorguyu değiştirip bir kez daha dene; yine olmazsa " + "aramanın yapılamadığını açıkça söyle, bilgi uydurma." +) + + +def python_yolu(calisan=None): + """Modelin calistiracagi yorumlayici. Kopru penceresiz (pythonw) calisiyorsa cikti veren python secilir.""" + calisan = calisan or sys.executable or "python" + klasor, ad = os.path.split(calisan) + if ad.lower() == "pythonw.exe": + aday = os.path.join(klasor, "python.exe") + if os.path.isfile(aday): + return aday + return calisan + + +def talimat(ayar, betik, python=None): + """Eklenecek metin; kapaliysa ya da betik yoksa None.""" + ayar = ayar if isinstance(ayar, dict) else {} + if ayar.get("enabled") is False or not betik or not os.path.isfile(betik): + return None + on = f'"{python or python_yolu()}" "{betik}"' + return str(ayar.get("talimat") or VARSAYILAN).replace("{ara}", on + " ara").replace("{oku}", on + " oku") + + +def ekle(req, metin): + """Notu istegin basina developer mesaji olarak koyar. True: eklendi (zaten varsa eklenmez).""" + girdi = req.get("input") + if not metin or not isinstance(girdi, list): + 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 ISARET in yazi: + return False + girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": metin}]}) + return True diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py index 2fb6a20..09eaa48 100644 --- a/apps/desktop/assets/codex-bridge/kopru.py +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -29,6 +29,7 @@ import pencere import yarim import model_gecis import is_hatti +import internet SURUM = "8" HERE = os.path.dirname(os.path.abspath(__file__)) @@ -1343,6 +1344,9 @@ class H(http.server.BaseHTTPRequestHandler): if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle( req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)): govde_degisti = True + # Ek modelde yerlesik web aramasi yok (flatten_request atar): aramayi kabukla yapan komut soylenir. + if internet.ekle(req, internet.talimat(CFG.get("internet"), os.path.join(HERE, "ara.py"))): + govde_degisti = True except Exception as e: log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}") # Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir. diff --git a/apps/desktop/assets/codex-bridge/model_gecis.py b/apps/desktop/assets/codex-bridge/model_gecis.py index a8a9c48..49ea418 100644 --- a/apps/desktop/assets/codex-bridge/model_gecis.py +++ b/apps/desktop/assets/codex-bridge/model_gecis.py @@ -15,6 +15,7 @@ Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday v """ import json import os +import collections import re import threading import time @@ -177,7 +178,11 @@ 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() + if len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit(): + return True + # Yarim soylenen ya da yarim yaziya dokulen soz adin basidir: "think" -> thinking, "obli" -> obliterated + # (olcum 11 Ekim 2026: "qwen 3.8 27b obli think" uc kez bulunamadi). + return len(sorgu) >= 4 and sorgu.isalpha() and parca.startswith(sorgu) def eslestir(ad, modeller): @@ -222,6 +227,14 @@ def eslestir(ad, modeller): 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]]} + # Adaylardan biri digerlerinin "fazladan sozcuklu" hali degilse sorulur. Biri digerlerinin govdesiyse + # ("...-obliterated" ile "...-obliterated:thinking") soylenmeyen eki tasimayan secilir: kullanici "thinking" + # demediyse dusunmeyeni istiyordur (olcum 11 Ekim 2026). Fark surum rakamiysa tahmin edilmez, sorulur. + sayim = [(collections.Counter(parcala(p[3]) or parcala(p[2])), p) for p in iyi] + sayim.sort(key=lambda c: sum(c[0].values())) + govde, secilen = sayim[0] + if all(not (govde - c) and (c - govde) and not any(k.isdigit() for k in (c - govde)) for c, _ in sayim[1:]): + return {"slug": secilen[2], "ad": secilen[3]} return {"adaylar": [p[3] for p in iyi[:4]]} diff --git a/apps/desktop/assets/codex-bridge/test_internet.py b/apps/desktop/assets/codex-bridge/test_internet.py new file mode 100644 index 0000000..e02da11 --- /dev/null +++ b/apps/desktop/assets/codex-bridge/test_internet.py @@ -0,0 +1,67 @@ +import os +import tempfile +import unittest + +import ara +import internet + +SAYFA = ''' + + + +''' + + +class AraTest(unittest.TestCase): + def test_results_are_parsed_and_ads_skipped(self): + self.assertEqual(ara.sonuclari_coz(SAYFA), [("Bir başlık", "https://ornek.com/bir?a=1", "Kısa & öz özet"), + ("İkinci", "https://ornek.org/iki", "")]) + self.assertEqual(len(ara.sonuclari_coz(SAYFA, adet=1)), 1) + + def test_search_output_and_empty_result(self): + metin = ara.ara("x", indirici=lambda adres: (SAYFA, "text/html")) + self.assertIn("1. Bir başlık\n https://ornek.com/bir?a=1", metin) + self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ("", "text/html"))) + + def test_page_is_turned_into_text_and_cut(self): + sayfa = "

Bir & iki

üç
" + self.assertEqual(ara.oku("ornek.com", indirici=lambda adres: (sayfa, "text/html")), "Bir & iki\nüç") + uzun = ara.oku("https://ornek.com", en_cok=10, indirici=lambda adres: ("

" + "a" * 50 + "

", "text/html")) + self.assertTrue(uzun.startswith("a" * 10 + "\n[... kesildi: 50")) + + def test_main_reports_errors_without_raising(self): + self.assertEqual(ara.main(["oku", "http://127.0.0.1:9/yok"]), 1) + + +class InternetTest(unittest.TestCase): + def test_note_names_both_commands_and_is_added_once(self): + with tempfile.TemporaryDirectory() as d: + betik = os.path.join(d, "ara.py") + open(betik, "w").close() + metin = internet.talimat(None, betik, python="py.exe") + self.assertIn(f'"py.exe" "{betik}" ara "sorgu"', metin) + self.assertIn(f'"py.exe" "{betik}" oku ', metin) + req = {"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "araştır"}]}]} + self.assertTrue(internet.ekle(req, metin)) + self.assertFalse(internet.ekle(req, metin)) + self.assertEqual(req["input"][0]["role"], "developer") + self.assertEqual(len(req["input"]), 2) + self.assertIsNone(internet.talimat({"enabled": False}, betik)) + self.assertIsNone(internet.talimat(None, os.path.join(d, "ara.py"))) # betik yoksa not eklenmez + + def test_windowless_python_is_swapped_for_the_console_one(self): + with tempfile.TemporaryDirectory() as d: + open(os.path.join(d, "python.exe"), "w").close() + self.assertEqual(internet.python_yolu(os.path.join(d, "pythonw.exe")), os.path.join(d, "python.exe")) + self.assertEqual(internet.python_yolu(os.path.join("yok", "pythonw.exe")), os.path.join("yok", "pythonw.exe")) + + +if __name__ == "__main__": + unittest.main() diff --git a/apps/desktop/assets/codex-bridge/test_model_gecis.py b/apps/desktop/assets/codex-bridge/test_model_gecis.py index 9bddabe..40fe27d 100644 --- a/apps/desktop/assets/codex-bridge/test_model_gecis.py +++ b/apps/desktop/assets/codex-bridge/test_model_gecis.py @@ -35,9 +35,25 @@ class Komut(unittest.TestCase): ("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_half_spoken_words_and_the_plain_variant(self): + """Measured 11 Oct 2026 in a voice session: "obli think" was not found three times, and "Qwen 3.8 + Obliterated" asked which one although the user had not said "thinking".""" + qwen = MODELS + [("ide/9router-nano-qwen-qwen3.8-27b-obliterated", "nano/qwen3.8-27b-obliterated"), + ("ide/9router-nano-qwen-qwen3.8-27b-obliterated-thinking", "nano/qwen3.8-27b-obliterated:thinking"), + ("ide/9router-nano-qwen-qwen3.8-27b-uncensored", "nano/qwen3.8-27b-uncensored")] + + def sec(text): + return (g.eslestir(g.komut_coz(text)["ad"], qwen) or {}).get("slug") + thinking = "ide/9router-nano-qwen-qwen3.8-27b-obliterated-thinking" + self.assertEqual(sec("Modeli 3.8 Obliterated Think yap"), thinking) + self.assertEqual(sec("Modeli Qwen... modeli Qwen 3.8 .27 B... Obli... Think yap"), thinking) + self.assertEqual(sec("Modeli Qwen ... 3.8 Obliterated yap"), "ide/9router-nano-qwen-qwen3.8-27b-obliterated") + self.assertEqual(sec("modeli klod opus yap"), "ide/9router-cc-claude-opus-5-5") # not the "(no-think)" one + self.assertIn("adaylar", g.eslestir(g.komut_coz("modeli qwen 3.8 yap")["ad"], qwen)) # obliterated or uncensored: ask + 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"]}) - self.assertIn("adaylar", pick("modeli klod opus yap")) # two Opus entries: ask + self.assertIn("adaylar", pick("modeli flash yap")) # several unrelated Flash models: ask self.assertIn("adaylar", pick("modeli sol yap")) # GPT-6-Sol or GPT-6.1-Sol: ask self.assertIn("adaylar", pick("modeli gpt 999 astra yap")) # words fit, version does not: ask