Compare commits

..
Author SHA1 Message Date
yilsemandClaude Opus 5.5 69daf809e0 fix(codex-link): recover Claude thinking-signature rejections the Hermes way
Server CI and Windows package / verify (push) Successful in 3m18s
Server CI and Windows package / windows-package (push) Successful in 15m5s
Claude rejects a turn whose latest assistant thinking blocks are not exactly
as produced ("cannot be modified" / "Invalid signature in thinking block").
Following Hermes (_manage_thinking_signatures: a dead-signature turn is
replayed without thinking), the bridge reads the first upstream piece and,
on exactly these messages, resends that one request once with reasoning
effort none before anything reaches Codex. Other errors are not resent.
The rejected request is kept as log/dusunme-400-*.json for diagnosis.

The overflow guard and layer filter still used a flat 2 bytes/token; they
now use the measured ratio. Ship IDE beta.48 (bridge version 27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-21 01:43:58 +03:00
yilsemandClaude Opus 5.5 f1802299f0 fix(codex-link): stop list re-downloads and count context by measurement
Server CI and Windows package / windows-package (push) Successful in 12m27s
Server CI and Windows package / verify (push) Successful in 4m37s
Keep the upstream model-list ETag: chatgpt.com repeats it in every answer
(x-models-etag), and the bridge's own stamp made Codex re-download the
~17 MB merged list after every answer (6198 times in one day).

Replace the flat 2 bytes/token estimate with measured ratios (673 real
requests): Claude family and unknown names 2.5, recognised non-Claude
families 3.4. Non-Claude chats were counted about twice as large, were
summarized too early and got false "does not fit" errors.

Ship IDE beta.47 (bridge version 26).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 23:40:43 +03:00
yilsemandClaude Opus 5.5 b347abe1f6 release(ide): ship beta.46 with original subscription WebSocket
Server CI and Windows package / verify (push) Successful in 5m42s
Server CI and Windows package / windows-package (push) Successful in 9m56s
Bridge version 25 so the installed IDE replaces the running splitter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 22:38:51 +03:00
yilsemandClaude Opus 5.5 be14cf2923 fix(codex-link): tunnel subscription WebSocket to chatgpt.com unchanged
Server CI and Windows package / windows-package (push) Successful in 10m51s
Server CI and Windows package / verify (push) Successful in 6m28s
Codex Baglama rejected every Responses WebSocket with 426, so Codex's own
subscription models fell back to HTTP and re-sent the whole conversation on
every step. The splitter now tunnels the WebSocket of Codex's own models to
chatgpt.com byte for byte (model known from x-codex-routing-hint). Added
models, remembered transfers and transfer history in a message still get
426 / a close frame and stay on the existing HTTP path, so added-model and
transition handling is unchanged. bridge.json "abonelik_websocket": false
restores the old refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 22:17:57 +03:00
15 changed files with 724 additions and 25 deletions

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
- name: Desktop browser control lane (AITURK Chrome) - name: Desktop browser control lane (AITURK Chrome)
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/gateway/test_browser_control_local_lane.py tests/gateway/test_browser_control_broker.py tests/gateway/test_browser_control_broker_hardening.py tests/gateway/test_browser_control_cloud.py tests/tools/test_browser_extension_router.py tests/tools/test_browser_extension_router_wiring.py -q -p no:cacheprovider -rf' run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/gateway/test_browser_control_local_lane.py tests/gateway/test_browser_control_broker.py tests/gateway/test_browser_control_broker_hardening.py tests/gateway/test_browser_control_cloud.py tests/tools/test_browser_extension_router.py tests/tools/test_browser_extension_router_wiring.py -q -p no:cacheprovider -rf'
- name: Native watch and speech mux pytest regressions - name: Native watch and speech mux pytest regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py apps/desktop/assets/codex-bridge/test_live_limits.py -q -p no:cacheprovider -rf' run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py apps/desktop/assets/codex-bridge/test_live_limits.py apps/desktop/assets/codex-bridge/test_dusunme_imzasi.py -q -p no:cacheprovider -rf'
- name: Mobile and desktop startup regression tests - name: Mobile and desktop startup regression tests
run: | run: |
npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1 npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
@@ -27,6 +27,39 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory. Tests: `python -m unittest discover` in this directory.
## Version 27: Claude thinking-signature rejection recovered the Hermes way (IDE beta.48)
- Claude rejects a turn when the thinking blocks of the latest assistant message are not exactly as it produced them
("thinking or redacted_thinking blocks in the latest assistant message cannot be modified", or "Invalid signature
in thinking block"). OmniRoute builds those blocks; seen 8 times in 48 h, always while Opus ran parallel sub-agents.
- Hermes rule (agent/anthropic_message_convert.py, _manage_thinking_signatures): a turn whose signature is dead is
replayed without its thinking. The bridge reads the first upstream piece; on exactly these two messages it resends
that one request once with `reasoning.effort = none` (history unchanged) before anything reaches Codex. Any other
error is not resent. The rejected request is kept for diagnosis as `log/dusunme-400-*.json` (newest 5).
- Needs the gateway to forward the provider reason (`detail`) in Responses stream errors (gateway 21 Oct 2026).
- The overflow guard and layer filter used a flat 2 bytes/token; they now use the measured ratio from version 26.
## Version 26: list downloaded once, context counted by measurement (IDE beta.47)
- Model list: the upstream stamp (ETag) is kept. chatgpt.com repeats it in every answer (`x-models-etag`); the
bridge's own stamp made Codex re-download the ~17 MB merged list after every answer (6198 times on 20 October
2026). Added-model changes still arrive on Codex's own 5-minute list refresh.
- Token estimate: measured on 673 real requests (bridge bytes against provider-reported input tokens). Claude
family and unknown names use 2.5 bytes/token (Claude lowest quartile 2.55); recognised non-Claude families use
3.4 (their lowest 5%). The old flat 2 bytes/token counted non-Claude chats about twice as large, summarized too
early and raised false "Son mesaj ve zorunlu talimatlar hedefe sigmiyor" errors.
## Version 25: subscription WebSocket stays original (IDE beta.46)
User decision (20 October 2026): "Codex orjinal aynen kalacak, sadece ek combo ve model ekleyeceğiz."
Codex opens a Responses WebSocket for its own models. The splitter used to refuse it with 426, so
subscription chats fell back to HTTP and re-sent the whole conversation on every step. Now:
- Codex's own model (`x-codex-routing-hint: model=...`): the WebSocket is tunnelled to chatgpt.com byte for byte.
- Added model, remembered transfer, or transfer history inside a message: 426 / close frame; that chat stays
on the existing HTTP path for the life of the Codex process, so added-model and transition handling is unchanged.
- `bridge.json` `"abonelik_websocket": false` restores the old refusal.
## Version 24: portable model switches and summary coverage (IDE beta.45) ## Version 24: portable model switches and summary coverage (IDE beta.45)
Cross-provider requests replace encrypted reasoning and every typed encrypted Cross-provider requests replace encrypted reasoning and every typed encrypted
+223 -7
View File
@@ -11,6 +11,8 @@ Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya g
(ide/..., 9r/..., model_map) o surece HIC gitmez. (ide/..., 9r/..., model_map) o surece HIC gitmez.
* Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi. * Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi.
* Geri kalan her sey -> chatgpt.com'a, oldugu gibi. * Geri kalan her sey -> chatgpt.com'a, oldugu gibi.
* Surekli baglanti (WebSocket) -> Codex'in kendi modeliyse chatgpt.com'a bayt bayt tunel (Codex'in ozgun hizli yolu).
Ek model ya da gecis gecmisi gorulurse o sohbet 426 ile HTTP'ye birakilir; ek model/gecis mantigi HTTP'de kalir.
Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik
inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider. inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider.
@@ -21,6 +23,11 @@ Yerel denetim: GET /aiturk/codex-baglama/status, POST .../stop, POST .../refresh
""" """
import gzip import gzip
import hashlib import hashlib
import socket
import ssl
import struct
import urllib.parse
import zlib
import http.client import http.client
import http.server import http.server
import json import json
@@ -35,7 +42,7 @@ import gecis_context
import terminal_stream import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "24" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION) SURUM = "27" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try: try:
with open(os.path.abspath(__file__), "rb") as _f: with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
@@ -160,6 +167,88 @@ def model_oku(govde, kodlama):
return None return None
# --- Surekli baglanti (WebSocket) ---------------------------------------------------------------------------
# Kullanici karari (20 Ekim 2026): "Codex orjinal aynen kalacakti, sadece ek combo ve model ekleyecektik."
# Codex abonelik modelinde chatgpt.com'a WebSocket acar; reddedilirse HTTP'ye duser ve her adimda butun sohbeti
# yeniden gonderir. Abonelik sohbeti bu yuzden chatgpt.com'a dokunulmadan tunellenir. Ek model ya da gecis
# gecmisi olan sohbet ilk anda 426 alir ve HTTP'de kalir (bugunku ek model/gecis davranisi aynen).
WS_HTTP = {} # session-id -> zaman: bu Codex surecinde HTTP'de kalacak sohbetler
ws_kilit = threading.Lock()
def ws_ipucu_model(baslik):
"""Codex'in el sikismadaki model ipucu (x-codex-routing-hint: model=gpt-6.1-sol)."""
for parca in (baslik or "").replace(",", ";").split(";"):
k, _, v = parca.partition("=")
if k.strip().lower() == "model":
return v.strip()
return ""
def ws_http_isaretle(sid):
if not sid:
return
with ws_kilit:
WS_HTTP[sid] = time.time()
while len(WS_HTTP) > 4096:
del WS_HTTP[next(iter(WS_HTTP))]
def ws_cerceve_oku(oku):
"""Tek cerceve: (ham_bayt, fin, rsv1, opcode, acik_yuk) ya da baglanti bittiyse None."""
bas = oku(2)
if len(bas) < 2:
return None
b0, b1 = bas[0], bas[1]
n, ek = b1 & 0x7F, b""
if n == 126:
ek = oku(2)
if len(ek) < 2:
return None
n = struct.unpack(">H", ek)[0]
elif n == 127:
ek = oku(8)
if len(ek) < 8:
return None
n = struct.unpack(">Q", ek)[0]
maske = oku(4) if b1 & 0x80 else b""
if b1 & 0x80 and len(maske) < 4:
return None
yuk = oku(n) if n else b""
if len(yuk) < n:
return None
ham = bas + ek + maske + yuk
if maske and n:
m = (maske * (n // 4 + 1))[:n]
yuk = (int.from_bytes(yuk, "big") ^ int.from_bytes(m, "big")).to_bytes(n, "big")
return ham, bool(b0 & 0x80), bool(b0 & 0x40), b0 & 0x0F, yuk
def ws_kapat_cercevesi(kod=1011, neden="AITURK: HTTP yoluna geciliyor"):
yuk = struct.pack(">H", kod) + neden.encode("utf-8")[:120]
return bytes([0x88, len(yuk)]) + yuk
def ws_mesaj_engeli(veri, hesap=""):
"""Bu mesaj chatgpt.com'a tunelden gidemez mi? Neden metni ya da None (gidebilir)."""
try:
m = json.loads(veri)
except (ValueError, UnicodeDecodeError):
return None
if not isinstance(m, dict) or m.get("type") != "response.create":
return None
if ek_model_mu(m.get("model")):
return "ek model"
if gecis_context.foreign(m) or gecis_context.foreign_history(m):
return "gecis gecmisi"
thread = str(m.get("prompt_cache_key") or "")
if thread:
scope = hashlib.sha256((str(hesap or "") + "|" + thread).encode()).hexdigest()
if ek_model_mu(TRANSFERS.get(scope) or ""):
return "gecis gecmisi"
return None
ISTEK_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"} ISTEK_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"}
YANIT_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"} YANIT_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"}
@@ -491,6 +580,138 @@ class H(http.server.BaseHTTPRequestHandler):
return None, True return None, True
return result["value"], started return result["value"], started
def _ws_ret(self, n, neden):
"""426: Codex bekletmeden HTTP yoluna duser (ek model ve gecisler bu yolda islenir)."""
log(f"[{n} WS] HTTP yoluna birakildi: {neden}")
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
self.close_connection = True
def _ws(self, n):
model = ws_ipucu_model(self.headers.get("x-codex-routing-hint"))
sid = self.headers.get("session-id") or self.headers.get("thread-id") or ""
hesap = self.headers.get("chatgpt-account-id") or ""
thread = self.headers.get("thread-id") or sid
scope = hashlib.sha256((str(hesap) + "|" + thread).encode()).hexdigest() if thread else ""
if CFG.get("abonelik_websocket") is False:
return self._ws_ret(n, "ayarla kapali")
if not self.path.split("?", 1)[0].rstrip("/").endswith("/responses"):
return self._ws_ret(n, "yol")
if not model:
return self._ws_ret(n, "model ipucu yok")
if ek_model_mu(model):
return self._ws_ret(n, f"ek model {model}")
with ws_kilit:
http_de = sid in WS_HTTP
if http_de:
return self._ws_ret(n, "sohbet HTTP yolunda")
if scope and ek_model_mu(TRANSFERS.get(scope) or ""):
return self._ws_ret(n, "gecis gecmisi")
u = urllib.parse.urlsplit(CHATGPT)
guvenli = u.scheme == "https"
try:
ust = socket.create_connection((u.hostname, u.port or (443 if guvenli else 80)), timeout=20)
if guvenli:
ust = ssl.create_default_context().wrap_socket(ust, server_hostname=u.hostname)
satirlar = [f"GET {self.path} HTTP/1.1", f"Host: {u.netloc}"]
satirlar += [f"{k}: {v}" for k, v in self.headers.items() if k.lower() != "host"]
ust.sendall(("\r\n".join(satirlar) + "\r\n\r\n").encode("latin-1"))
cevap = b""
while b"\r\n\r\n" not in cevap:
parca = ust.recv(65536)
if not parca or len(cevap) > 65536:
raise OSError("el sikisma cevabi eksik")
cevap += parca
bas, _, artik = cevap.partition(b"\r\n\r\n")
durum = int(bas.split(b" ", 2)[1])
except (OSError, ValueError, IndexError) as e:
return self._ws_ret(n, f"chatgpt.com acilamadi: {type(e).__name__}: {e}")
if durum != 101:
ust.close()
return self._ws_ret(n, f"chatgpt.com {durum}")
uzanti = ""
for satir in bas.split(b"\r\n")[1:]:
k, _, v = satir.decode("latin-1").partition(":")
if k.strip().lower() == "sec-websocket-extensions":
uzanti = v.strip().lower()
sikistirma = "permessage-deflate" in uzanti
baglamsiz = "client_no_context_takeover" in uzanti
self.close_connection = True
yaz_kilit = threading.Lock()
bitti = threading.Event()
ust.settimeout(None)
self.connection.settimeout(None)
def istemciye(veri):
with yaz_kilit:
self.wfile.write(veri)
self.wfile.flush()
def kapat():
bitti.set()
for soket in (ust, self.connection):
try:
soket.shutdown(socket.SHUT_RDWR)
except OSError:
pass
istemciye(bas + b"\r\n\r\n" + artik)
log(f"[{n} WS {model}] chatgpt.com'a dogrudan tunel acildi")
def asagi():
try:
while True:
parca = ust.recv(65536)
if not parca:
break
istemciye(parca)
except OSError:
pass
finally:
kapat()
threading.Thread(target=asagi, daemon=True).start()
acici = zlib.decompressobj(-zlib.MAX_WBITS) if sikistirma else None
ham_parcalar, yukler, sikisik, mesaj = [], [], False, 0
try:
while not bitti.is_set():
c = ws_cerceve_oku(self.rfile.read)
if c is None:
break
ham, fin, rsv1, op, yuk = c
if op >= 8:
ust.sendall(ham)
continue
if op in (1, 2):
ham_parcalar, yukler, sikisik = [ham], [yuk], rsv1
else:
ham_parcalar.append(ham)
yukler.append(yuk)
if not fin:
continue
veri = b"".join(yukler)
if sikisik and acici is not None:
if baglamsiz:
acici = zlib.decompressobj(-zlib.MAX_WBITS)
veri = acici.decompress(veri + b"\x00\x00\xff\xff")
neden = ws_mesaj_engeli(veri, hesap)
if neden:
ws_http_isaretle(sid)
log(f"[{n} WS {model}] {neden}: sohbet HTTP yoluna aktarildi ({mesaj} mesaj tunelden gitti)")
istemciye(ws_kapat_cercevesi())
break
ust.sendall(b"".join(ham_parcalar))
ham_parcalar, yukler = [], []
mesaj += 1
except (OSError, zlib.error) as e:
log(f"[{n} WS {model}] baglanti hatasi: {type(e).__name__}: {e}")
finally:
kapat()
ust.close()
log(f"[{n} WS {model}] kapandi, {mesaj} mesaj")
def _isle(self): def _isle(self):
self._native_return_scope = None self._native_return_scope = None
self._failure_key = None self._failure_key = None
@@ -498,12 +719,7 @@ class H(http.server.BaseHTTPRequestHandler):
sira[0] += 1 sira[0] += 1
n = sira[0] n = sira[0]
if "websocket" in (self.headers.get("Upgrade") or "").lower(): if "websocket" in (self.headers.get("Upgrade") or "").lower():
# Bugunku calisan durum: surekli acik baglanti reddedilir, Codex olagan yola duser. return self._ws(n)
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
return
govde = self._oku() govde = self._oku()
yol = self.path yol = self.path
if yol.startswith("/aiturk/codex-baglama/"): if yol.startswith("/aiturk/codex-baglama/"):
@@ -68,9 +68,29 @@ def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
# Bytes per token, measured on 673 real added-model requests (20 October 2026; bridge request bytes against the
# provider's reported input tokens): Claude family 2.55 at the lowest quartile (median 2.58); every other family
# 3.4 or more at the lowest 5% (Gemini, GPT, abliteration, Jail-Omni, bekci ~3.7-4.2). The old flat 2 bytes/token
# counted non-Claude chats about twice as large, summarized too early and raised false "does not fit" errors.
# Claude, profiles and anything unrecognised keep the safe 2.5; only a recognised non-Claude family gets 3.4.
SAFE_BYTES_PER_TOKEN = 2.5
NON_CLAUDE_BYTES_PER_TOKEN = 3.4
CLAUDE_HINTS = ("claude", "anthropic", "opus", "sonnet", "haiku")
NON_CLAUDE_HINTS = ("gemini", "gemma", "gpt", "grok", "deepseek", "qwen", "glm", "kimi", "llama", "mistral",
"nemotron", "minimax", "seed-", "mimo", "abliterat")
def bytes_per_token(model):
slug = str(model or "").lower()
if not slug or any(h in slug for h in CLAUDE_HINTS):
return SAFE_BYTES_PER_TOKEN
return NON_CLAUDE_BYTES_PER_TOKEN if any(h in slug for h in NON_CLAUDE_HINTS) else SAFE_BYTES_PER_TOKEN
def tokens(value): def tokens(value):
# Deliberately more conservative than the old 3.2 bytes/token estimate. """Conservative token estimate. A request (dict with "model") uses that model's measured ratio."""
return (len(encoded(value).encode("utf-8")) + 1) // 2 model = value.get("model") if isinstance(value, dict) else None
return int(len(encoded(value).encode("utf-8")) / bytes_per_token(model)) + 1
def input_budget(req, window, settings=None, automatic=False): def input_budget(req, window, settings=None, automatic=False):
+79 -7
View File
@@ -127,6 +127,54 @@ UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir. # Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir.
NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"]) NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"])
LOGDIR = os.path.join(HOME, "log") LOGDIR = os.path.join(HOME, "log")
# Hermes'in Anthropic imza hatalari (agent/anthropic_message_convert.py): "thinking blocks in the latest assistant
# message cannot be modified" ve "Invalid signature in thinking block". Gecit ayrintiyi "detail" alaninda iletir.
DUSUNME_IMZA_HATASI = re.compile(r"(thinking|redacted_thinking)[^\n]{0,160}(cannot be modified|must remain as they were)"
r"|invalid signature in thinking block", re.I)
DUSUNME_ORNEK_SAYI = 5 # tesis icin saklanan son istek sayisi (yalniz bu bilgisayarda, log klasorunde)
def dusunme_imzasi_bozuk(ilk):
"""Ust kaynagin ilk parcasi Claude'un dusunme-imzasi reddi mi? (yalniz "error" olayinda bakilir)"""
metin = ilk.decode("utf-8", "replace") if isinstance(ilk, (bytes, bytearray)) else str(ilk or "")
return ('"error"' in metin or "event: error" in metin) and bool(DUSUNME_IMZA_HATASI.search(metin))
def dusunme_ornegi_sakla(req, ilk):
"""Teshis: reddedilen istegi yerel log klasorune yazar; en yeni DUSUNME_ORNEK_SAYI dosya kalir."""
try:
os.makedirs(LOGDIR, exist_ok=True)
ad = os.path.join(LOGDIR, time.strftime("dusunme-400-%Y%m%d-%H%M%S-") + f"{threading.get_ident() % 100000}.json")
with open(ad, "w", encoding="utf-8") as f:
json.dump({"hata": ilk.decode("utf-8", "replace")[:2000], "istek": req}, f, ensure_ascii=False)
eski = sorted(x for x in os.listdir(LOGDIR) if x.startswith("dusunme-400-"))
for x in eski[:-DUSUNME_ORNEK_SAYI]:
os.remove(os.path.join(LOGDIR, x))
return ad
except OSError:
return None
class OnekliYanit:
"""Okunmus ilk parcayi once veren yanit sarmalayicisi; geri kalani ustteki yanittan gelir."""
def __init__(self, yanit, onek):
self.yanit, self.onek = yanit, onek
def __getattr__(self, ad):
return getattr(self.yanit, ad)
def read1(self, n=-1):
if self.onek:
parca, self.onek = self.onek, b""
return parca
return self.yanit.read1(n)
def read(self, n=-1):
if self.onek:
parca, self.onek = self.onek, b""
return parca + (self.yanit.read(n) if n is None or n < 0 else b"")
return self.yanit.read(n)
os.makedirs(LOGDIR, exist_ok=True) os.makedirs(LOGDIR, exist_ok=True)
lock = threading.Lock() lock = threading.Lock()
log_lock = threading.Lock() log_lock = threading.Lock()
@@ -373,8 +421,7 @@ def sigan_katmanlar(sira, req, tag):
bilgi = IDE_BILGI.get(IDE_PREFIX + ad) or {} bilgi = IDE_BILGI.get(IDE_PREFIX + ad) or {}
p = bilgi.get("pencere") p = bilgi.get("pencere")
if p and i < len(sira) - 1 and pencere.bekci_calisir(bilgi.get("kaynak"), p): if p and i < len(sira) - 1 and pencere.bekci_calisir(bilgi.get("kaynak"), p):
if tahmin is None: tahmin = pencere.tahmini_token(req, gecis_context.bytes_per_token(ad))
tahmin = pencere.tahmini_token(req, 2.0)
if tahmin > int(p * 0.90): if tahmin > int(p * 0.90):
log(f"{tag} katman {ad} atlandi: istek sigmaz (tahmini {tahmin} token, pencere {p})") log(f"{tag} katman {ad} atlandi: istek sigmaz (tahmini {tahmin} token, pencere {p})")
continue continue
@@ -1628,7 +1675,6 @@ class H(http.server.BaseHTTPRequestHandler):
out = raw; changed = 0 out = raw; changed = 0
raw_len = len(out) raw_len = len(out)
duz = out # sikistirilmamis govde: yedege bu yazilir duz = out # sikistirilmamis govde: yedege bu yazilir
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
enc_out = None enc_out = None
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed: if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
import gzip import gzip
@@ -1637,8 +1683,9 @@ class H(http.server.BaseHTTPRequestHandler):
for k, v in resp.headers.items(): for k, v in resp.headers.items():
kl = k.lower() kl = k.lower()
if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue
if kl in ("etag", "x-models-etag") and changed: # The upstream list stamp is kept (20 October 2026). chatgpt.com sends the same stamp in every answer
v = catalog_etag # (x-models-etag); a different stamp here made Codex re-download this ~17 MB list after every answer
# (6198 times in one day). Added-model changes still arrive on Codex's own 5-minute list refresh.
hdrs_out.append((k, v)) hdrs_out.append((k, v))
if changed: if changed:
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de). # Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
@@ -1959,7 +2006,9 @@ class H(http.server.BaseHTTPRequestHandler):
if not hedef_pencere or not pencere.bekci_calisir(pencere_kaynagi, hedef_pencere): if not hedef_pencere or not pencere.bekci_calisir(pencere_kaynagi, hedef_pencere):
hedef_pencere = 32768 hedef_pencere = 32768
if CFG.get("tasma_bekcisi", True) and not ozet_tetik: if CFG.get("tasma_bekcisi", True) and not ozet_tetik:
tahmin = pencere.tahmini_token(req, 2.0) # Olcume dayali oran (21 Ekim 2026): duz 2 bayt/token Claude disi sohbetleri iki kat buyuk sayip
# sahte "sigmiyor" veriyordu; beta.47'de gecis_context duzeltilmis, bu ikinci yer kalmisti.
tahmin = pencere.tahmini_token(req, gecis_context.bytes_per_token(slug))
sigdi = tahmin <= int(hedef_pencere * 0.90) sigdi = tahmin <= int(hedef_pencere * 0.90)
if rewriter is not None: if rewriter is not None:
rewriter.dolu = tahmin >= hedef_pencere * 0.7 rewriter.dolu = tahmin >= hedef_pencere * 0.7
@@ -2048,7 +2097,9 @@ class H(http.server.BaseHTTPRequestHandler):
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026); # have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
# only failures before the work starts (no connection, 500/502/503/529) are retried. # only failures before the work starts (no connection, 500/502/503/529) are retried.
combo = "/" not in target combo = "/" not in target
for attempt in range(1, RETRIES + 1): denemeler = list(range(1, RETRIES + 1))
dusunme_tekrar = False
for attempt in denemeler:
r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST") r = urllib.request.Request(route["url"], data=data, headers=hdrs, method="POST")
try: try:
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT) resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
@@ -2092,6 +2143,27 @@ class H(http.server.BaseHTTPRequestHandler):
break break
log(f"{tag} deneme {attempt}: {e}") log(f"{tag} deneme {attempt}: {e}")
time.sleep(min(2 * attempt, 8)); continue time.sleep(min(2 * attempt, 8)); continue
if "text/event-stream" in str(resp.headers.get("Content-Type", "")):
try:
ilk = resp.read1(4096)
except (OSError, http.client.HTTPException):
ilk = b""
if not dusunme_tekrar and dusunme_imzasi_bozuk(ilk):
# Hermes: imzasi olen son tur dusunmesiz yeniden gonderilir. Bir kez; istemciye bir sey yazilmadi.
dusunme_tekrar = True
ornek = dusunme_ornegi_sakla(req, ilk)
log(f"{tag} Claude dusunme imzasi reddi: istek bir kez dusunme kapali yeniden gonderiliyor"
f"{' (ornek ' + os.path.basename(ornek) + ')' if ornek else ''}")
try:
resp.close()
except Exception:
pass
req["reasoning"] = {"effort": "none"}
req.pop("include", None)
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
denemeler.append(attempt)
continue
resp = OnekliYanit(resp, ilk)
# Only the resolved exact voice target owns this sideband protocol. # Only the resolved exact voice target owns this sideband protocol.
# Other models retain their original response/Jev/stream pipeline. # Other models retain their original response/Jev/stream pipeline.
if jev_denetim.izleme_mi(target): if jev_denetim.izleme_mi(target):
@@ -33,7 +33,7 @@ class AccessPrograms(unittest.TestCase):
"fix_tool_ids": lambda req: 0, "fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0, "drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(), "SseRewriter": Mock(),
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], "log": lambda message: None, "pencere": __import__("pencere"), "gecis_context": __import__("gecis_context"), "dusunme_imzasi_bozuk": __import__("kopru").dusunme_imzasi_bozuk, "dusunme_ornegi_sakla": __import__("kopru").dusunme_ornegi_sakla, "OnekliYanit": __import__("kopru").OnekliYanit, "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900, "DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900,
} }
exec(compile(ast.Module(body=taraf + [flatten, claude_sonu, responses, ozgun], type_ignores=[]), "bridge.py", "exec"), self.env) exec(compile(ast.Module(body=taraf + [flatten, claude_sonu, responses, ozgun], type_ignores=[]), "bridge.py", "exec"), self.env)
@@ -6,10 +6,13 @@ import hashlib
import http.client import http.client
import http.server import http.server
import json import json
import socket
import struct
import tempfile import tempfile
import threading import threading
import time import time
import unittest import unittest
import zlib
import bridge import bridge
@@ -57,6 +60,70 @@ def zstd(veri):
return zstandard.ZstdCompressor().compress(veri) return zstandard.ZstdCompressor().compress(veri)
def ws_cerceve(veri, op=1, rsv1=False, maske=b"\x01\x02\x03\x04"):
"""Istemci (Codex) cercevesi: maskeli."""
b0 = 0x80 | (0x40 if rsv1 else 0) | op
n = len(veri)
bas = bytes([b0, 0x80 | n]) if n < 126 else bytes([b0, 0x80 | 126]) + struct.pack(">H", n) if n < 65536 \
else bytes([b0, 0x80 | 127]) + struct.pack(">Q", n)
return bas + maske + bytes(c ^ maske[i % 4] for i, c in enumerate(veri))
def sikistir(sik, veri):
return (sik.compress(veri) + sik.flush(zlib.Z_SYNC_FLUSH))[:-4]
class SahteWs:
"""Sahte chatgpt.com WebSocket ucu: el sikismayi ve gelen ham cerceveleri kaydeder, her mesaja ozetle cevap verir."""
def __init__(self, durum=101, uzanti=""):
self.durum, self.uzanti, self.ham, self.baslik, self.baglanti = durum, uzanti, [], {}, 0
self.dinle = socket.socket()
self.dinle.bind(("127.0.0.1", 0))
self.dinle.listen(8)
self.port = self.dinle.getsockname()[1]
threading.Thread(target=self._kabul, daemon=True).start()
def _kabul(self):
while True:
try:
s, _ = self.dinle.accept()
except OSError:
return
self.baglanti += 1
threading.Thread(target=self._bag, args=(s,), daemon=True).start()
def _bag(self, s):
f = s.makefile("rb")
satirlar = []
while True:
satir = f.readline().decode("latin-1").rstrip("\r\n")
if not satir:
break
satirlar.append(satir)
for satir in satirlar[1:]:
k, _, v = satir.partition(":")
self.baslik[k.strip().lower()] = v.strip()
if self.durum != 101:
s.sendall(b"HTTP/1.1 %d Hayir\r\nContent-Length: 0\r\n\r\n" % self.durum)
s.close()
return
cevap = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
if self.uzanti:
cevap += f"Sec-WebSocket-Extensions: {self.uzanti}\r\n"
s.sendall((cevap + "\r\n").encode())
ac = zlib.decompressobj(-zlib.MAX_WBITS)
while True:
c = bridge.ws_cerceve_oku(f.read)
if c is None or c[3] == 8:
break
self.ham.append(c[0])
veri = ac.decompress(c[4] + b"\x00\x00\xff\xff") if c[2] else c[4]
yuk = b"yanit:" + hashlib.sha256(veri).hexdigest().encode()
s.sendall(bytes([0x81, len(yuk)]) + yuk)
s.close()
class Ayirici(unittest.TestCase): class Ayirici(unittest.TestCase):
def setUp(self): def setUp(self):
self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream")) self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream"))
@@ -72,6 +139,7 @@ class Ayirici(unittest.TestCase):
self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS) self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS)
bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json" bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json"
bridge.TRANSFERS.clear() bridge.TRANSFERS.clear()
bridge.WS_HTTP.clear()
bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}" bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}"
bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}" bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}"
bridge.LOGDIR = self.gecici.name bridge.LOGDIR = self.gecici.name
@@ -209,11 +277,107 @@ class Ayirici(unittest.TestCase):
d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1]) d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1])
self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True)) self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True))
def test_a_persistent_connection_is_refused_the_way_it_is_today(self): def test_a_persistent_connection_without_a_model_hint_is_refused_the_way_it_is_today(self):
durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"}) durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"})
self.assertEqual(durum, 426) self.assertEqual(durum, 426)
self.assertEqual(self.ozgun.gelen + self.kopru.gelen, []) self.assertEqual(self.ozgun.gelen + self.kopru.gelen, [])
# --- Surekli baglanti: abonelik modeli chatgpt.com'a tunellenir, ek model/gecis HTTP'de kalir ---------------
def ws_ust(self, durum=101, uzanti=""):
ust = SahteWs(durum, uzanti)
bridge.CHATGPT = f"http://127.0.0.1:{ust.port}"
return ust
def ws_ac(self, model="gpt-6.1-sol", sid="sohbet-1", uzanti=True):
k = socket.create_connection(("127.0.0.1", self.on.server_address[1]), timeout=10)
baslik = ["GET /backend-api/codex/responses HTTP/1.1", "Host: 127.0.0.1", "Connection: Upgrade",
"Upgrade: websocket", "Sec-WebSocket-Version: 13", "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
"authorization: Bearer sahte", "chatgpt-account-id: hesap", f"session-id: {sid}", f"thread-id: {sid}",
"openai-beta: responses_websockets=2026-02-06"]
if model:
baslik.append(f"x-codex-routing-hint: model={model}")
if uzanti:
baslik.append("sec-websocket-extensions: permessage-deflate; client_max_window_bits")
k.sendall(("\r\n".join(baslik) + "\r\n\r\n").encode())
cevap = b""
while b"\r\n\r\n" not in cevap:
parca = k.recv(4096)
if not parca:
break
cevap += parca
return k, int(cevap.split(b" ", 2)[1]), cevap.partition(b"\r\n\r\n")[2]
def test_codex_own_model_websocket_is_tunnelled_to_chatgpt_byte_for_byte(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac()
self.assertEqual(durum, 101)
mesaj = json.dumps({"type": "response.create", "model": "gpt-6.1-sol", "input": [{"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "selam " * 5000}]}]}).encode()
cerceve = ws_cerceve(mesaj)
k.sendall(cerceve)
cevap = bridge.ws_cerceve_oku(k.makefile("rb").read)
self.assertEqual(cevap[4], b"yanit:" + hashlib.sha256(mesaj).hexdigest().encode())
self.assertEqual(ust.ham[0], cerceve) # Codex'in cercevesi aynen gitti
self.assertEqual(ust.baslik["authorization"], "Bearer sahte")
self.assertEqual(ust.baslik["openai-beta"], "responses_websockets=2026-02-06")
self.assertEqual(self.kopru.gelen, [])
k.close()
def test_added_model_websocket_falls_back_to_http_at_once(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac(model="ide/claude-claude-haiku-5-5")
self.assertEqual(durum, 426)
self.assertEqual(ust.baglanti, 0)
k.close()
def test_switching_to_an_added_model_on_an_open_tunnel_moves_the_chat_to_http(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac(sid="sohbet-gecis")
self.assertEqual(durum, 101)
k.sendall(ws_cerceve(json.dumps({"type": "response.create", "model": "ide/agy-gemini-3.8-flash-low", "input": []}).encode()))
kapanis = bridge.ws_cerceve_oku(k.makefile("rb").read)
self.assertEqual(kapanis[3], 8) # kapanis cercevesi
self.assertEqual(ust.ham, []) # ek model istegi chatgpt.com'a gitmedi
k.close()
k2, durum2, _ = self.ws_ac(sid="sohbet-gecis")
self.assertEqual(durum2, 426) # ayni sohbet artik HTTP yolunda
k2.close()
def test_compressed_message_with_transfer_history_is_detected(self):
ust = self.ws_ust(uzanti="permessage-deflate")
k, durum, _ = self.ws_ac(sid="sohbet-sikisik")
self.assertEqual(durum, 101)
sik = zlib.compressobj(wbits=-zlib.MAX_WBITS)
ilk = json.dumps({"type": "response.create", "model": "gpt-6.1-sol", "input": [{"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "ilk"}]}]}).encode()
k.sendall(ws_cerceve(sikistir(sik, ilk), rsv1=True))
okur = k.makefile("rb").read
self.assertEqual(bridge.ws_cerceve_oku(okur)[3], 1)
ikinci = json.dumps({"type": "response.create", "model": "gpt-6.1-sol",
"input": [{"type": "message", "role": "assistant", "id": "chatcmpl-abc", "content": []}]}).encode()
k.sendall(ws_cerceve(sikistir(sik, ikinci), rsv1=True))
self.assertEqual(bridge.ws_cerceve_oku(okur)[3], 8)
self.assertEqual(len(ust.ham), 1)
k.close()
def test_chat_with_a_remembered_transfer_stays_on_http(self):
self.ws_ust()
scope = hashlib.sha256(b"hesap|sohbet-eski").hexdigest()
bridge.TRANSFERS[scope] = "ide/Jail-Omni"
k, durum, _ = self.ws_ac(sid="sohbet-eski")
self.assertEqual(durum, 426)
k.close()
def test_chatgpt_refusing_the_tunnel_or_setting_off_falls_back_to_http(self):
ust = self.ws_ust(durum=401)
k, durum, _ = self.ws_ac(sid="sohbet-red")
self.assertEqual((durum, ust.baglanti), (426, 1))
k.close()
bridge.CFG["abonelik_websocket"] = False
k, durum, _ = self.ws_ac(sid="sohbet-kapali")
self.assertEqual((durum, ust.baglanti), (426, 1))
k.close()
def test_which_side_a_model_is_on(self): def test_which_side_a_model_is_on(self):
for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False), for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False),
("", False), (None, False), ("idea", False)): ("", False), (None, False), ("idea", False)):
@@ -40,7 +40,7 @@ class ComboNotResent(unittest.TestCase):
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0, "fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(), "log": lambda message: None, "SseRewriter": Mock(), "log": lambda message: None,
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600, "DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [], "NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "gecis_context": __import__("gecis_context"), "dusunme_imzasi_bozuk": __import__("kopru").dusunme_imzasi_bozuk, "dusunme_ornegi_sakla": __import__("kopru").dusunme_ornegi_sakla, "OnekliYanit": __import__("kopru").OnekliYanit, "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
} }
exec(compile(ast.Module(body=[pick("ek_model_mu"), pick("yonlendirilen"), pick("flatten_request"), pick("claude_sonu_duzelt"), exec(compile(ast.Module(body=[pick("ek_model_mu"), pick("yonlendirilen"), pick("flatten_request"), pick("claude_sonu_duzelt"),
pick("is_timeout"), pick("kopma_mi"), responses], pick("is_timeout"), pick("kopma_mi"), responses],
@@ -40,6 +40,11 @@ def method(name):
def build(nodes, env): def build(nodes, env):
# Names the real module defines at import time (beta.48: measured token ratio, thinking-signature resend).
env.setdefault("gecis_context", __import__("gecis_context"))
env.setdefault("dusunme_imzasi_bozuk", __import__("kopru").dusunme_imzasi_bozuk)
env.setdefault("dusunme_ornegi_sakla", __import__("kopru").dusunme_ornegi_sakla)
env.setdefault("OnekliYanit", __import__("kopru").OnekliYanit)
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env) exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
return env return env
@@ -483,6 +488,19 @@ class Liste(unittest.TestCase):
self.assertIn(("Content-Encoding", "gzip"), fake.sent) self.assertIn(("Content-Encoding", "gzip"), fake.sent)
env["save_last_models"].assert_called_once() env["save_last_models"].assert_called_once()
def test_the_list_keeps_the_upstream_stamp_so_codex_does_not_refetch_after_every_answer(self):
"""chatgpt.com repeats its list stamp (x-models-etag) in every answer; a changed stamp here made Codex
download the whole list again after each answer (6198 times on 20 October 2026)."""
env = self.env(EXTRA_MODELS=[{"slug": "ide/a", "display_name": "Aaa"}])
fake = Sahte({"chatgpt-account-id": "hesap"}, "GET")
resp = types.SimpleNamespace(read=lambda: json.dumps(self.UPSTREAM).encode(),
headers={"ETag": 'W/"40b107fc"', "x-models-etag": 'W/"40b107fc"'})
env["_models"](fake, resp, "[t]")
sent = dict(fake.sent)
self.assertEqual((sent["ETag"], sent["x-models-etag"]), ('W/"40b107fc"', 'W/"40b107fc"'))
self.assertIn("ide/a", [m["slug"] for m in json.loads(fake.wfile.getvalue())["models"]])
self.assertEqual(dict(env["LAST_MODELS"]["hesap"]["headers"])["ETag"], 'W/"40b107fc"')
def test_no_network_still_serves_the_last_list(self): def test_no_network_still_serves_the_last_list(self):
cached = {"headers": [("Content-Type", "application/json")], "body": b'{"models": []}', "at": "x"} cached = {"headers": [("Content-Type", "application/json")], "body": b'{"models": []}', "at": "x"}
env = self.env(LAST_MODELS={"hesap": cached}) env = self.env(LAST_MODELS={"hesap": cached})
@@ -0,0 +1,145 @@
"""Claude thinking-signature rejection: one clean resend without thinking (Hermes rule), nothing else retried.
Hermes (agent/anthropic_message_convert.py, _manage_thinking_signatures): when the latest turn's thinking
signature is dead, the turn is replayed without it. The bridge cannot touch the blocks OmniRoute builds, so
it resends that one request once with thinking off before anything reaches Codex. Real sockets, no network.
"""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import http.client
import json
import os
import threading
import pytest
import gecis_context
import kopru
import pencere
IMZA = ("[400]: messages.11.content.1: `thinking` or `redacted_thinking` blocks in the latest assistant message "
"cannot be modified. These blocks must remain as they were in the original response.")
def hata_olayi(detail):
body = {"type": "error", "error": {"type": "upstream_error", "message": "upstream 400", "detail": detail}}
return b"event: error\ndata: " + json.dumps(body).encode() + b"\n\n"
TAMAM = (b'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"TAMAM"}\n\n'
b'event: response.completed\ndata: {"type":"response.completed","response":{"id":"r","status":"completed","output":[]}}\n\n')
class Ust:
"""Fake gateway: answers each request with the next scripted SSE body and records the requests."""
def __init__(self, cevaplar):
self.istekler, self.cevaplar = [], list(cevaplar)
sahip = self
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_POST(self):
sahip.istekler.append(json.loads(self.rfile.read(int(self.headers["Content-Length"]))))
govde = sahip.cevaplar.pop(0) if sahip.cevaplar else TAMAM
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
self.server = ThreadingHTTPServer(("127.0.0.1", 0), H)
self.server.daemon_threads = True
threading.Thread(target=self.server.serve_forever, daemon=True).start()
self.url = f"http://127.0.0.1:{self.server.server_port}/v1/responses"
def kapat(self):
self.server.shutdown()
self.server.server_close()
@pytest.fixture
def kurulum(tmp_path, monkeypatch):
def kur(cevaplar, slug="ide/claude-claude-opus-5-5", hedef="claude-claude-opus-5-5", bilgi=None):
ust = Ust(cevaplar)
monkeypatch.setattr(kopru, "WATCH_OWNER", None)
monkeypatch.setattr(kopru, "LOGDIR", str(tmp_path))
monkeypatch.setattr(kopru.jev_denetim, "anahtar", lambda: "")
monkeypatch.setitem(kopru.MODEL_MAP, slug, ("aiturk-ide", hedef, None))
monkeypatch.setitem(kopru.ROUTES, "aiturk-ide", {"url": ust.url, "key": "uye", "flatten_tools": False})
if bilgi:
monkeypatch.setitem(kopru.IDE_BILGI, slug, bilgi)
sunucu = kopru.TS(("127.0.0.1", 0), kopru.H)
threading.Thread(target=sunucu.serve_forever, daemon=True).start()
def iste(girdi=None):
b = http.client.HTTPConnection("127.0.0.1", sunucu.server_port, timeout=10)
istek = {"model": slug, "stream": True, "reasoning": {"effort": "medium", "summary": "detailed"},
"include": ["reasoning.encrypted_content"],
"input": girdi or [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "selam"}]}]}
b.request("POST", "/v1/responses", json.dumps(istek), {"Content-Type": "application/json"})
y = b.getresponse()
veri = y.read()
b.close()
return y.status, veri
kur.kapanacak = [ust.kapat, sunucu.shutdown, sunucu.server_close]
return ust, iste
yield kur
for k in getattr(kur, "kapanacak", []):
k()
def test_both_hermes_signature_messages_are_recognised_only_in_an_error_event():
assert kopru.dusunme_imzasi_bozuk(hata_olayi(IMZA))
assert kopru.dusunme_imzasi_bozuk(hata_olayi("[400]: messages.3.content.0: Invalid signature in thinking block"))
assert not kopru.dusunme_imzasi_bozuk(hata_olayi("upstream 400"))
assert not kopru.dusunme_imzasi_bozuk(b'data: {"type":"response.output_text.delta","delta":"thinking blocks cannot be modified"}\n\n')
def test_signature_rejection_is_resent_once_without_thinking_and_codex_sees_no_error(kurulum, tmp_path):
ust, iste = kurulum([hata_olayi(IMZA), TAMAM])
durum, veri = iste()
assert durum == 200 and b"TAMAM" in veri and b"error" not in veri
assert len(ust.istekler) == 2
assert ust.istekler[0]["reasoning"]["effort"] == "medium" and "include" in ust.istekler[0]
assert ust.istekler[1]["reasoning"] == {"effort": "none"} and "include" not in ust.istekler[1]
assert ust.istekler[1]["input"] == ust.istekler[0]["input"] # history itself untouched
ornekler = [x for x in os.listdir(tmp_path) if x.startswith("dusunme-400-")]
assert len(ornekler) == 1
kayit = json.loads((tmp_path / ornekler[0]).read_text(encoding="utf-8"))
assert "cannot be modified" in kayit["hata"] and kayit["istek"]["input"] == ust.istekler[0]["input"]
def test_any_other_upstream_error_is_not_resent(kurulum):
ust, iste = kurulum([hata_olayi("[400]: messages.0: some other provider error")])
durum, veri = iste()
assert len(ust.istekler) == 1
assert b"error" in veri or b"failed" in veri
def test_a_second_signature_rejection_is_not_resent_again(kurulum):
ust, iste = kurulum([hata_olayi(IMZA), hata_olayi(IMZA), TAMAM])
iste()
assert len(ust.istekler) == 2
def test_a_normal_stream_keeps_its_first_piece(kurulum):
ust, iste = kurulum([TAMAM])
durum, veri = iste()
assert durum == 200 and b"TAMAM" in veri and b"response.completed" in veri
assert len(ust.istekler) == 1 and ust.istekler[0]["reasoning"]["effort"] == "medium"
def test_overflow_guard_counts_with_the_measured_ratio(kurulum):
"""~100k real tokens for a 128k Gemini: the old 2 bytes/token guard refused it before sending."""
metin = "x" * 330_000
girdi = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": metin}]}]
slug = "ide/agy-gemini-3.8-flash-low"
assert pencere.tahmini_token({"input": girdi}, 2.0) > int(128000 * 0.90) # old: refused
assert pencere.tahmini_token({"input": girdi}, gecis_context.bytes_per_token(slug)) < int(128000 * 0.90)
ust, iste = kurulum([TAMAM], slug=slug, hedef="agy-gemini-3.8-flash-low",
bilgi={"pencere": 128000, "kaynak": "saglayici-katalogu"})
durum, veri = iste(girdi)
assert len(ust.istekler) == 1 and b"TAMAM" in veri
assert "tahmini_token(req, 2.0)" not in open(kopru.__file__, encoding="utf-8").read()
@@ -195,8 +195,9 @@ class PreparationService(unittest.TestCase):
def test_output_reserve_can_require_preparation_below_seventy_percent(self): def test_output_reserve_can_require_preparation_below_seventy_percent(self):
req = {"model": "ide/test", "max_output_tokens": 24000, req = {"model": "ide/test", "max_output_tokens": 24000,
"input": [{"role": "user", "content": "x" * 20000}, {"role": "user", "content": "latest"}]} "input": [{"role": "user", "content": "x" * 30000}, {"role": "user", "content": "latest"}]}
self.assertLess(gc.tokens(req), int(32768 * .7)) self.assertLess(gc.tokens(req), int(32768 * .7))
self.assertGreater(gc.tokens(req), 32768 - 24000)
result = self.service._prepare_transfer({"request": req})["request"] result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), 32768 - 24000) self.assertLessEqual(gc.tokens(result), 32768 - 24000)
self.assertTrue(self.server.gelen) self.assertTrue(self.server.gelen)
@@ -446,5 +447,35 @@ class SocketMigration(fixtures.Ayirici):
self.assertEqual(len(self.kopru.gelen), 1) self.assertEqual(len(self.kopru.gelen), 1)
class TokenEstimate(unittest.TestCase):
"""Measured bytes/token (20 October 2026): Claude ~2.55, other families >= 3.4. Unknown stays safe."""
def test_ratio_follows_the_measured_model_family_and_unknown_stays_safe(self):
for slug, ratio in (("ide/claude-claude-haiku-5-5", 2.5), ("ide/claude-claude-opus-5-5", 2.5),
("ide/nanogpt-Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled", 2.5),
("ide/Jail-Omni", 2.5), ("ide/Takim-Hizli", 2.5), ("", 2.5), (None, 2.5),
("ide/agy-gemini-3.8-flash-low", 3.4), ("gpt-6.1-sol", 3.4), ("ide/deepseek-deepseek-flash", 3.4),
("ide/abliteration-abliterated-model-large-v2", 3.4), ("ide/grok-cli-grok-4.7", 3.4)):
self.assertEqual(gc.bytes_per_token(slug), ratio, slug)
def test_estimate_never_counts_fewer_tokens_than_the_measured_lowest_ratio(self):
text = "Türkçe ve English karışık bir metin, kod: def f(x): return x * 2. " * 4000
size = len(gc.encoded({"model": "ide/claude-claude-opus-5-5", "input": text}).encode("utf-8"))
self.assertGreaterEqual(gc.tokens({"model": "ide/claude-claude-opus-5-5", "input": text}), size / 2.55)
size = len(gc.encoded({"model": "ide/agy-gemini-3.8-flash-low", "input": text}).encode("utf-8"))
self.assertGreaterEqual(gc.tokens({"model": "ide/agy-gemini-3.8-flash-low", "input": text}), size / 3.41)
self.assertGreater(gc.tokens([text]), gc.tokens({"model": "gpt-6.1-sol", "input": text})) # no model: safe
def test_a_chat_that_really_fits_a_non_claude_target_is_not_refused(self):
"""Before: 2 bytes/token counted this ~75k-token chat as ~130k and refused it for a 128k model."""
turn = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "soru " + "x" * 64000}]},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "cevap " + "y" * 64000}]}]
req = {"model": "ide/agy-gemini-3.8-flash-low", "input": turn * 2}
self.assertLess(gc.tokens(req), int(128000 * 0.70))
self.assertGreater(len(gc.encoded(req).encode("utf-8")) // 2, int(128000 * 0.70)) # old estimate refused it
prepared = gc.Preparer().prepare(req, 128000, lambda text, budget: self.fail("must not summarize"))
self.assertEqual(prepared["input"], req["input"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
@@ -336,7 +336,7 @@ class TurBasi(Ev):
class KatmanPenceresi(unittest.TestCase): class KatmanPenceresi(unittest.TestCase):
def suzgec(self, bilgi): def suzgec(self, bilgi):
logs = [] logs = []
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': bilgi, 'pencere': pencere, 'log': logs.append} env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': bilgi, 'pencere': pencere, 'log': logs.append, 'gecis_context': __import__('gecis_context')}
exec(compile(ast.Module(body=[fonksiyon('sigan_katmanlar')], type_ignores=[]), 'kopru.py', 'exec'), env) exec(compile(ast.Module(body=[fonksiyon('sigan_katmanlar')], type_ignores=[]), 'kopru.py', 'exec'), env)
return env['sigan_katmanlar'], logs return env['sigan_katmanlar'], logs
+1 -1
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama' export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports // Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive. // another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '24' export const CODEX_BRIDGE_VERSION = '27'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}` export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide", "name": "aiturk-ide",
"productName": "AITURK IDE", "productName": "AITURK IDE",
"private": true, "private": true,
"version": "1.0.0-beta.45", "version": "1.0.0-beta.48",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis", "author": "AITURK / TurkServis",
"repository": { "repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
}, },
"apps/desktop": { "apps/desktop": {
"name": "aiturk-ide", "name": "aiturk-ide",
"version": "1.0.0-beta.45", "version": "1.0.0-beta.48",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",