Compare commits

...
Author SHA1 Message Date
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
10 changed files with 482 additions and 17 deletions

No files matched your search

@@ -27,6 +27,27 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory.
## 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)
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.
* Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi.
* 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
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 hashlib
import socket
import ssl
import struct
import urllib.parse
import zlib
import http.client
import http.server
import json
@@ -35,7 +42,7 @@ import gecis_context
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "24" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "26" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try:
with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
@@ -160,6 +167,88 @@ def model_oku(govde, kodlama):
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"}
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 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):
self._native_return_scope = None
self._failure_key = None
@@ -498,12 +719,7 @@ class H(http.server.BaseHTTPRequestHandler):
sira[0] += 1
n = sira[0]
if "websocket" in (self.headers.get("Upgrade") or "").lower():
# Bugunku calisan durum: surekli acik baglanti reddedilir, Codex olagan yola duser.
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
return
return self._ws(n)
govde = self._oku()
yol = self.path
if yol.startswith("/aiturk/codex-baglama/"):
@@ -68,9 +68,29 @@ def encoded(value):
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):
# Deliberately more conservative than the old 3.2 bytes/token estimate.
return (len(encoded(value).encode("utf-8")) + 1) // 2
"""Conservative token estimate. A request (dict with "model") uses that model's measured ratio."""
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):
+3 -3
View File
@@ -1628,7 +1628,6 @@ class H(http.server.BaseHTTPRequestHandler):
out = raw; changed = 0
raw_len = len(out)
duz = out # sikistirilmamis govde: yedege bu yazilir
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
enc_out = None
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
import gzip
@@ -1637,8 +1636,9 @@ class H(http.server.BaseHTTPRequestHandler):
for k, v in resp.headers.items():
kl = k.lower()
if kl in RESP_HOP or kl in ("date", "set-cookie", "cf-ray"): continue
if kl in ("etag", "x-models-etag") and changed:
v = catalog_etag
# The upstream list stamp is kept (20 October 2026). chatgpt.com sends the same stamp in every answer
# (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))
if changed:
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
@@ -6,10 +6,13 @@ import hashlib
import http.client
import http.server
import json
import socket
import struct
import tempfile
import threading
import time
import unittest
import zlib
import bridge
@@ -57,6 +60,70 @@ def zstd(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):
def setUp(self):
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)
bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json"
bridge.TRANSFERS.clear()
bridge.WS_HTTP.clear()
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.LOGDIR = self.gecici.name
@@ -209,11 +277,107 @@ class Ayirici(unittest.TestCase):
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))
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"})
self.assertEqual(durum, 426)
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):
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)):
@@ -483,6 +483,19 @@ class Liste(unittest.TestCase):
self.assertIn(("Content-Encoding", "gzip"), fake.sent)
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):
cached = {"headers": [("Content-Type", "application/json")], "body": b'{"models": []}', "at": "x"}
env = self.env(LAST_MODELS={"hesap": cached})
@@ -195,8 +195,9 @@ class PreparationService(unittest.TestCase):
def test_output_reserve_can_require_preparation_below_seventy_percent(self):
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.assertGreater(gc.tokens(req), 32768 - 24000)
result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), 32768 - 24000)
self.assertTrue(self.server.gelen)
@@ -446,5 +447,35 @@ class SocketMigration(fixtures.Ayirici):
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__":
unittest.main()
+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'
// 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.
export const CODEX_BRIDGE_VERSION = '24'
export const CODEX_BRIDGE_VERSION = '26'
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'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.45",
"version": "1.0.0-beta.47",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.45",
"version": "1.0.0-beta.47",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",