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 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 07:25:30 +03:00
1 parent 23e8aa8191
commit c8993d58cc
6 files changed
+265 -2

No files matched your search

+106
View File
@@ -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 <adres> [--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'<div[^>]+class="[^"]*\bresult\b[^"]*"', sayfa)[1:]:
bag = re.search(r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>', 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"[^>]*>(.*?)</a>', 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.*?</\1>", " ", 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 "<html" in sayfa[:2000].lower() else sayfa
if len(metin) > 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())
@@ -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} <adres>\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
@@ -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.
@@ -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]]}
@@ -0,0 +1,67 @@
import os
import tempfile
import unittest
import ara
import internet
SAYFA = '''
<div class="result results_links results_links_deep web-result ">
<h2 class="result__title"><a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fornek.com%2Fbir%3Fa%3D1&amp;rut=x">Bir <b>başlık</b></a></h2>
<a class="result__snippet" href="//x">Kısa &amp; öz <b>özet</b></a>
</div>
<div class="result results_links web-result ">
<a class="result__a" href="https://duckduckgo.com/y.js?ad=1">Reklam</a>
</div>
<div class="result results_links web-result ">
<a class="result__a" href="https://ornek.org/iki">İkinci</a>
</div>
'''
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: ("<html></html>", "text/html")))
def test_page_is_turned_into_text_and_cut(self):
sayfa = "<html><head><style>a{}</style><script>x()</script></head><body><nav>menü</nav><p>Bir &amp; iki</p><div>üç</div></body></html>"
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: ("<p>" + "a" * 50 + "</p>", "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 <adres>', 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()
@@ -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