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.
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
try:
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"):
d["item"]["phase"] = phase
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):
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
t = d.get("type", "")
@@ -517,10 +536,7 @@ class SseRewriter:
if pencere.aracsiz_mi(metin):
if self.aracsiz is not None:
self.aracsiz(self.hedef)
return [{"type": "response.failed", "response": {
"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."}}}]
return self._gorunur_hata("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):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
@@ -535,11 +551,12 @@ class SseRewriter:
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"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):
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key"
d = {"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}}
t = "response.failed"
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
f"Ayrıntı: {metin[:200]}")
if pencere.kapali_hata_mi(metin):
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
if item and item.get("type") in ARAC_OGELERI:
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()
try:
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
total += len(chunk)
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"])):
# 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.
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
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)):