fix(codex-bridge): audit findings: visible permanent errors, stricter model command, unbuffered stream

From ten independent read-only audits (11 Oct 2026), verified against the code:
- a permanent gateway error (no tools, 401/402/403, bare upstream 400) ends the turn with a visible
  assistant message instead of a silent retry and an empty turn;
- "modeli ... yapma", "X modeli için test yap" and general "hangi model ..." questions are no longer
  commands; a name whose version does not fit, or that several models share, is asked back;
- the stream is forwarded as it arrives (read1) and an upstream drop is no longer logged as a client
  stop nor saved as an interrupted answer;
- a lone "." joining a running turn is removed without the hook messages after it;
- the interrupted-answer note finds the user message behind hook messages and detects completion
  by event type; a per-minute rate limit is not learned as a context window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-11 01:23:07 +03:00
1 parent 1e2b8e2b9c
commit e2793fa38f
7 files changed
+129 -32

No files matched your search

+37 -12
View File
@@ -15,7 +15,7 @@ Data directory (bridge.json, log/): --home <dir> or AITURK_CODEX_BRIDGE_HOME,
otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME. otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME.
Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop. Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop.
""" """
import http.server, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib import http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
from gemini_context import coalesce_context from gemini_context import coalesce_context
try: try:
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
@@ -502,6 +502,25 @@ class SseRewriter:
if d is not None and not d["item"].get("phase"): if d is not None and not d["item"].get("phase"):
d["item"]["phase"] = phase d["item"]["phase"] = phase
return d return d
def _gorunur_hata(self, mesaj):
"""Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026)
hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda
kullanici hatayi duyar."""
damga = "%d" % int(time.time() * 1000)
rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
"content": [{"type": "output_text", "text": mesaj, "annotations": []}]}
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
return [
{"type": "response.output_item.added", "output_index": 0,
"item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}},
{"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj},
{"type": "response.output_item.done", "output_index": 0, "item": oge},
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
"output": [oge], "usage": kullanim}},
]
def _sirala(self, d): def _sirala(self, d):
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir).""" """Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
t = d.get("type", "") t = d.get("type", "")
@@ -517,10 +536,7 @@ class SseRewriter:
if pencere.aracsiz_mi(metin): if pencere.aracsiz_mi(metin):
if self.aracsiz is not None: if self.aracsiz is not None:
self.aracsiz(self.hedef) self.aracsiz(self.hedef)
return [{"type": "response.failed", "response": { return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "invalid_request_error", "message":
"Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin."}}}]
if self.dolu and pencere.kapali_hata_mi(metin): if self.dolu and pencere.kapali_hata_mi(metin):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma # Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil). # yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
@@ -535,11 +551,12 @@ class SseRewriter:
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], "id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}] "error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I): if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key" neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
d = {"type": "response.failed", "response": { return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [], f"Ayrıntı: {metin[:200]}")
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}} if pencere.kapali_hata_mi(metin):
t = "response.failed" return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
"yeniden deneyin.")
item = d.get("item") if isinstance(d.get("item"), dict) else None item = d.get("item") if isinstance(d.get("item"), dict) else None
if item and item.get("type") in ARAC_OGELERI: if item and item.get("type") in ARAC_OGELERI:
self.arac_goruldu = True self.arac_goruldu = True
@@ -768,7 +785,15 @@ class H(http.server.BaseHTTPRequestHandler):
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush() self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
try: try:
while True: while True:
chunk = resp.read(4096) try:
# read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu.
chunk = resp.read1(4096)
except (OSError, http.client.HTTPException) as e:
# Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex
# yarim akisi tam sanmasin ve yeniden denesin.
log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}")
self.close_connection = True
return
if not chunk: break if not chunk: break
total += len(chunk) total += len(chunk)
if len(bas) < 600: if len(bas) < 600:
@@ -1142,7 +1167,7 @@ class H(http.server.BaseHTTPRequestHandler):
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])): if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida # Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar. # keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
del req["input"][model_gecis.son_sira(req["input"]):] del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir
govde_degisti = True govde_degisti = True
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi") log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)): if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
@@ -26,7 +26,14 @@ EN_COK_KELIME = 14
_KILIT = threading.Lock() _KILIT = threading.Lock()
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu") _TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla") # 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") SORU = ("hangi", "hangisi", "kim", "kimdir")
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir", 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", "olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
@@ -100,8 +107,10 @@ def komut_coz(metin):
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler): if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
return None return None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler): if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"} return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")] if any(k.startswith(OLUMSUZ) for k in kelimeler):
return None
fiil = [k for k in kelimeler if k in FIILLER]
if fiil: if fiil:
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil] 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 = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
@@ -131,15 +140,33 @@ def eslestir(ad, modeller):
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume)) 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)) 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 if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
puanli.append((tutan, -len(parcalar), slug, gorunen)) 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: if not puanli:
return None return None
en = max(p[0] for p in puanli) en = max(p[0] for p in puanli)
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3): harfler = [s for s in ad if not s.isdigit()]
return None # sozun cogu hicbir modele uymuyor: komut degil en_harf = max(p[4] for p in puanli)
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3])) if en_harf < len(harfler):
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]: 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]} 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]]} return {"adaylar": [p[3] for p in iyi[:4]]}
+11 -1
View File
@@ -146,12 +146,22 @@ def kapali_hata_mi(metin):
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I)) return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
def hiz_siniri_mi(metin):
"""Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
return bool(_HIZ_SINIRI.search(metin or ""))
def tasma_mi(metin): def tasma_mi(metin):
return bool(_TASMA.search(metin or "")) return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
def sinir_oku(metin): def sinir_oku(metin):
"""Hedefin tasma hatasindaki gercek sinir; yoksa None.""" """Hedefin tasma hatasindaki gercek sinir; yoksa None."""
if hiz_siniri_mi(metin):
return None
for kalip in _SINIR: for kalip in _SINIR:
m = kalip.search(metin or "") m = kalip.search(metin or "")
if m and int(m.group(1)) >= TABAN: if m and int(m.group(1)) >= TABAN:
@@ -32,8 +32,11 @@ class Ek(unittest.TestCase):
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message": flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'} '[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
out = feed(flat, hedef="m", aracsiz=seen.append) out = feed(flat, hedef="m", aracsiz=seen.append)
self.assertEqual(out[0]["type"], "response.failed") # A permanent error ends the turn with a visible answer instead of a silent retry.
self.assertIn("araç çağrısını desteklemiyor", out[0]["response"]["error"]["message"]) self.assertEqual([e["type"] for e in out], ["response.output_item.added", "response.output_text.delta",
"response.output_item.done", "response.completed"])
self.assertIn("araç çağrısını desteklemiyor", out[1]["delta"])
self.assertEqual(out[2]["item"]["phase"], "final_answer")
self.assertEqual(seen, ["m"]) self.assertEqual(seen, ["m"])
home = tempfile.mkdtemp() home = tempfile.mkdtemp()
self.assertTrue(pencere.ogren_aracsiz(home, "m")) self.assertTrue(pencere.ogren_aracsiz(home, "m"))
@@ -29,14 +29,24 @@ class Komut(unittest.TestCase):
def test_spoken_names_resolve_to_one_model(self): def test_spoken_names_resolve_to_one_model(self):
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"), for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"), ("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
("modeli klod opus yap", "ide/9router-cc-claude-opus-5-5"), ("modeli klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
("modeli opusa geç", "ide/9router-cc-claude-opus-5-5"), ("modeli opus 5 5'e geç", "ide/9router-cc-claude-opus-5-5"),
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"), ("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]: ("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) self.assertEqual((pick(text) or {}).get("slug"), slug, text)
def test_ambiguous_name_lists_candidates_and_changes_nothing(self): 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.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 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
def test_negations_and_general_questions_are_not_commands(self):
for text in ("modeli gemini flash low yapma", "modeli değiştirme opus kalsın", "GPT 6 Astra modeli için test yap",
"hangi model daha ucuz", "en iyi model hangisi", "modeli seçili dosyaya göre gemini flash low yap"):
result = pick(text)
self.assertFalse(result and ("slug" in result or result.get("tur") == "sor"), text)
self.assertEqual(g.komut_coz("hangi modelsin"), {"tur": "sor"})
def test_ordinary_sentences_are_not_commands(self): def test_ordinary_sentences_are_not_commands(self):
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir", for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
@@ -77,7 +87,7 @@ class TurOrtasi(unittest.TestCase):
class Gecmis(unittest.TestCase): class Gecmis(unittest.TestCase):
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self): def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"), req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
voice("modeli opus yap"), user("modeli gpt 6 astra yap")]} voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2) self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"]) self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var") self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
@@ -17,7 +17,7 @@ for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets): {"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
nodes.insert(0, node) nodes.insert(0, node)
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": pencere} env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env) exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
NANO = pencere.nano_sadelestir({"data": [ NANO = pencere.nano_sadelestir({"data": [
@@ -196,9 +196,13 @@ class GatewayErrors(unittest.TestCase):
adapter = env["SseRewriter"](set()) adapter = env["SseRewriter"](set())
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}}) raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
out = parse(adapter.feed(raw) + adapter.flush()) out = parse(adapter.feed(raw) + adapter.flush())
self.assertEqual(out[0]["type"], "response.failed") self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
self.assertEqual(out[0]["response"]["error"]["code"], "insufficient_quota") self.assertIn("kredi ya da kota", out[1]["delta"])
self.assertIn("upstream 402", out[0]["response"]["error"]["message"]) self.assertIn("upstream 402", out[1]["delta"])
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 400"}})
out = parse(env["SseRewriter"](set()).feed(raw))
self.assertIn("nedenini bildirmedi", out[1]["delta"])
self.assertEqual(out[-1]["type"], "response.completed")
# A transient error is left alone so that Codex retries it. # A transient error is left alone so that Codex retries it.
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}}) raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error") self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
@@ -238,6 +242,11 @@ class ClaudeOrani(unittest.TestCase):
self.assertEqual((trimmed, fits), (1, True)) self.assertEqual((trimmed, fits), (1, True))
self.assertLess(estimate, 194_000) self.assertLess(estimate, 194_000)
self.assertTrue(p.kapali_hata_mi("upstream 400")) self.assertTrue(p.kapali_hata_mi("upstream 400"))
# A per-minute rate limit is neither an overflow nor a window size to learn.
rate = "Rate limit reached: limit of 32768 tokens per minute, context window ok"
self.assertFalse(p.tasma_mi(rate))
self.assertIsNone(p.sinir_oku(rate))
self.assertTrue(p.tasma_mi("maximum context length is 65536 tokens"))
self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema")) self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema"))
if __name__ == "__main__": if __name__ == "__main__":
+15 -2
View File
@@ -48,7 +48,15 @@ class Izleyici:
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str): if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
self.parcalar.append(d["delta"]) self.parcalar.append(d["delta"])
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok: elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
self.bitti = True # Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
tur = json.loads(satir[5:]).get("type")
except (ValueError, AttributeError):
continue
if tur in ("response.completed", "response.failed", "response.incomplete"):
self.bitti = True
@property @property
def metin(self): def metin(self):
@@ -89,7 +97,12 @@ def uygula(req, anahtar, simdi=None):
girdi = req.get("input") girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi: if not kayit or not isinstance(girdi, list) or not girdi:
return 0 return 0
son = girdi[-1] son = None
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
continue
son = oge
break
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"): if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"] metin = kayit["metin"]