fix(codex-bridge): preserve tool model and stream preparation without Jev fallback

This commit is contained in:
yilsem committed 2026-10-11 15:51:43 +03:00
1 parent 8fbbe226db
commit 3726d2a553
6 files changed
+118 -14

No files matched your search

+16 -1
View File
@@ -13,6 +13,7 @@ import re
import sys
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
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/?"
@@ -73,7 +74,21 @@ 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."
if 'no-results' in sayfa.lower() or 'no results found' in sayfa.lower():
return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin."
# DDG bot kontrolünü/bozuk HTML'yi sonuç yok diye göstermeyiz. Tek yedek istek.
try:
rss, _ = indirici('https://www.bing.com/search?' + urllib.parse.urlencode({'format': 'rss', 'q': sorgu}))
kok = ET.fromstring(rss)
if kok.tag != 'rss' or kok.find('channel') is None:
raise ValueError('RSS arama yanıtı değil')
sonuc = [(i.findtext('title') or '', i.findtext('link') or '', _duz(i.findtext('description') or ''))
for i in kok.findall('./channel/item')
if (i.findtext('link') or '').startswith(('https://', 'http://'))][:adet]
except (ValueError, ET.ParseError, OSError) as hata:
raise RuntimeError('Arama hizmeti engellendi veya yanıtı okunamadı; sorguları değiştirerek tekrar etme.') from hata
if not sonuc:
return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin."
return "\n\n".join(f"{i}. {b}\n {a}\n {o}" for i, (b, a, o) in enumerate(sonuc, 1))
+3 -3
View File
@@ -18,8 +18,8 @@ VARSAYILAN = (
"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."
"arama (tool_search ile bulunmaz). Arama hizmeti engeli veya ayrıştırma hatasında sorguları tekrar tekrar "
"değiştirme; aramanın yapılamadığını açıkça söyle. Gerçek sonuç yoksa en fazla bir farklı sorgu dene. Bilgi uydurma."
)
@@ -39,7 +39,7 @@ def talimat(ayar, betik, python=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}"'
on = f'& "{python or python_yolu()}" "{betik}"' if os.name == 'nt' else f'"{python or python_yolu()}" "{betik}"'
return str(ayar.get("talimat") or VARSAYILAN).replace("{ara}", on + " ara").replace("{oku}", on + " oku")
@@ -93,6 +93,32 @@ def son_istek(req):
return ""
def hazirlik_mi(govde):
"""Araç çağrısı veya commentary görüldüyse adım nihai cevap değildir.
Tam SSE satırları üzerinden bakılır; parçalı olay bir sonraki parçada tamamlanır.
Araç argümanında geçen olay adı hazırlık sayılmaz.
"""
for satir in govde.splitlines():
if not satir.startswith(b'data:'):
continue
try:
olay = json.loads(satir[5:])
except (ValueError, UnicodeDecodeError):
continue
if not isinstance(olay, dict):
continue
tip = str(olay.get('type') or '')
oge = olay.get('item') or {}
if tip in ('response.output_item.added', 'response.output_item.done') and isinstance(oge, dict):
if str(oge.get('type') or '').endswith('_call') or oge.get('phase') == 'commentary':
return True
if tip in ('response.function_call_arguments.delta', 'response.function_call_arguments.done',
'response.custom_tool_call_input.delta', 'response.custom_tool_call_input.done'):
return True
return False
def komutlar(govde):
"""Yazisi olmayan adimda Jev'e giden metin: modelin bu adimda cagirdigi araclar (ad ve arguman).
govde: ust kaynagin SSE yaniti (bayt). Arac cagrisi yoksa ""."""
+11 -2
View File
@@ -901,11 +901,20 @@ class H(http.server.BaseHTTPRequestHandler):
break
parcalar.append(chunk)
okuyan.feed(chunk)
if jev_denetim.hazirlik_mi(b''.join(parcalar)):
# Kullanıcı kararı 11 Ekim: araç/hazırlık adımı ret denetimine girmez.
# İlk araç olayı görüldüğünde tamponu bırak; araç çağrısının bitmesini bekleme.
log(f"{tag} Jev atlandi: arac veya hazirlik adimi, secili model korunuyor")
for parca in parcalar:
if izle is not None:
izle.feed(parca)
w(rewriter.feed(parca) if rewriter else parca)
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
if time.time() - son_yazi >= 10:
w(b": denetim\n\n"); son_yazi = time.time()
yazi = okuyan.metin.strip()
# Her adim Jev'e gider: yazi yoksa modelin bu adimda cagirdigi araclar sorulur.
metin = yazi or jev_denetim.komutlar(b"".join(parcalar))
# Araç/hazırlık yukarıda aktarıldı; burada yalnız nihai cevap denetlenir.
metin = yazi
gecmedi = False
if hata:
gecmedi = True
@@ -28,7 +28,20 @@ class AraTest(unittest.TestCase):
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")))
self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ('<div class="no-results">no results</div>', "text/html")))
def test_bot_challenge_uses_one_rss_backup_not_false_no_results(self):
calls = []
def indirici(adres):
calls.append(adres)
return (('<form id="challenge-form">Bot challenge</form>' if len(calls) == 1 else
'<rss><channel><item><title>OmniRoute</title><link>https://ornek.com/</link></item></channel></rss>'), 'text/html')
self.assertIn('https://ornek.com/', ara.ara('OmniRoute', indirici=indirici))
self.assertEqual(len(calls), 2)
def test_two_invalid_services_report_failure_not_no_results(self):
with self.assertRaisesRegex(RuntimeError, 'Arama hizmeti'):
ara.ara('x', indirici=lambda adres: ('<html>challenge</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>"
@@ -46,7 +59,8 @@ class InternetTest(unittest.TestCase):
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)
on = '& ' if os.name == 'nt' else ''
self.assertIn(f'{on}"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))
@@ -127,6 +127,7 @@ class Akis(unittest.TestCase):
sahte.sor = self.sor
sahte.karar = jev_denetim.karar
sahte.komutlar = jev_denetim.komutlar
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
"log": self.logs.append, "RESP_HOP": set(), "json": json}
adlar = ("_akis_basligi", "_denetimli_akis", "_stream")
@@ -216,17 +217,56 @@ class Akis(unittest.TestCase):
KOMUT = (b'event: response.output_item.done\ndata: {"type": "response.output_item.done", "item": '
b'{"type": "function_call", "name": "shell", "arguments": "{\\"cmd\\": \\"dir\\"}"}}\n\n')
def test_a_step_without_text_is_asked_with_its_tool_calls(self):
def test_a_step_without_text_passes_without_jev(self):
body = self.KOMUT + sse()
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, ['[komut] shell: {"cmd": "dir"}'])
self.assertEqual(self.asked, [])
self.assertEqual(self.opened, [])
def test_a_blocked_step_without_text_goes_to_the_next_layer(self):
def test_tool_step_cannot_be_replaced_even_if_jev_would_refuse(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.calistir(self.KOMUT + sse())
self.assertEqual(self.opened, ["Katman-2"])
self.assertEqual(self.sent(), self.cevap["Katman-2"])
body = self.KOMUT + sse()
self.calistir(body)
self.assertEqual(self.opened, [])
self.assertEqual(self.asked, [])
self.assertEqual(self.sent(), body)
def test_tool_event_reaches_client_before_upstream_finishes(self):
first = (b'data: {"type":"response.output_item.added","item":'
b'{"type":"function_call","name":"shell","arguments":""}}\n\n')
owner = self
class Streaming(Yanit):
def __init__(self):
super().__init__(b'')
self.parts = [first, owner.KOMUT + sse()]
self.reads = 0
def read1(self, n):
self.reads += 1
if self.reads == 2:
owner.assertIn(first, owner.h.wfile.getvalue())
owner.assertEqual(owner.asked, [])
owner.assertEqual(owner.opened, [])
return super().read1(n)
self.h._denetimli_akis(Streaming(), '[1]', None, None, 'yap', ['Katman-1'], self.ac)
self.assertEqual(self.sent(), first + self.KOMUT + sse())
def test_text_with_a_tool_call_is_preparation_not_a_final_refusal(self):
body = sse('Önce dosyaları inceleyeceğim.') + self.KOMUT
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
self.assertEqual(self.opened, [])
def test_explicit_commentary_passes_without_jev(self):
body = (b'data: {"type":"response.output_item.added","item":'
b'{"type":"message","role":"assistant","phase":"commentary"}}\n\n' + sse('Araştırıyorum.'))
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
def test_an_empty_step_of_the_selected_model_goes_to_the_next_layer(self):
self.calistir(sse()) # no text, no tool call