Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1802299f0 | ||
|
|
b347abe1f6 | ||
|
|
be14cf2923 | ||
|
|
1f13b86e5f | ||
|
|
7a479e46be | ||
|
|
db46a470ea | ||
|
|
21a8f3b8e9 | ||
|
|
fb62f31be3 | ||
|
|
1bf80e06a3 | ||
|
|
69e2f98e18 | ||
|
|
0d8c521a3c |
No files matched your search
@@ -5,6 +5,10 @@ on:
|
||||
branches: [aiturk/main, fix/ide-windows-vcruntime]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -19,6 +23,16 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
github-server-url: https://gitea.turkservis.online
|
||||
- name: Require main and Windows release to share the exact source
|
||||
run: |
|
||||
set -eu
|
||||
main_sha="$(git ls-remote origin refs/heads/aiturk/main | cut -f1)"
|
||||
release_sha="$(git ls-remote origin refs/heads/fix/ide-windows-vcruntime | cut -f1)"
|
||||
if [ "$main_sha" != "$GITHUB_SHA" ] || [ "$release_sha" != "$GITHUB_SHA" ]; then
|
||||
echo "Both IDE branches must be updated together to the same merged commit."
|
||||
echo "Use one atomic push; refusing to publish a divergent branch."
|
||||
exit 1
|
||||
fi
|
||||
- name: Exact source and Linux test environment
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
@@ -27,6 +27,54 @@ 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
|
||||
peer-message part with a short visible omission note. Ciphertext is never
|
||||
interpreted, guessed from its alphabet, or forwarded to another provider. Visible
|
||||
messages, peer text, tool calls/results and their order remain intact. Projection
|
||||
is idempotent and changes only the outgoing copy. Returning to a subscription
|
||||
model retains its original encrypted execution state, including large histories.
|
||||
Actual conversation compaction is still rendered through its issuing endpoint;
|
||||
unreadable conversation summaries retain the existing explicit error.
|
||||
|
||||
Other external-provider switches reuse the same transcript, with destination
|
||||
context preparation only when necessary. This follows the Hermes runtime model
|
||||
switch contract: `agent/agent_runtime_helpers.py::switch_model` updates the client,
|
||||
provider, cache policy and compressor without resetting conversation history;
|
||||
`website/docs/user-guide/configuring-models.md` documents in-session `/model`.
|
||||
Native voice routing is unchanged.
|
||||
|
||||
The compression target is a maximum, not a minimum length. All of the available
|
||||
target budget is offered to the summarizer; compression level controls prose
|
||||
density and no longer multiplies that budget down a second time. Numbered
|
||||
decision identifiers, file references and numeric settings must survive the
|
||||
summary before it is accepted or cached. An incomplete provider summary tries
|
||||
the configured fallback; a deficient old checkpoint is ignored and rebuilt.
|
||||
These literal checks detect concrete omissions; they do not prove semantic
|
||||
equivalence of every sentence.
|
||||
|
||||
## Version 19: decision stages (14 October 2026)
|
||||
|
||||
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
|
||||
|
||||
@@ -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 = "22" # 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/"):
|
||||
@@ -521,16 +737,6 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
except Exception:
|
||||
req = {}
|
||||
if external:
|
||||
# Opaque peer tasks cannot be sent to another provider. Detect
|
||||
# this permanent transfer error before committing SSE headers:
|
||||
# stock Codex retries unknown response.failed stream errors.
|
||||
# Projection validates a copy; the saved/request history stays
|
||||
# intact and no task is dropped or sent to the worker.
|
||||
try:
|
||||
gecis_context.portable_reasoning(req)
|
||||
except gecis_context.TransferError as e:
|
||||
return self._json(400, {"error": {"type": "invalid_request_error",
|
||||
"code": "aiturk_transfer_failed", "message": str(e), "retryable": False}})
|
||||
self._failure_key = terminal_stream.request_key(req or {"raw": govde.hex()}, self.headers)
|
||||
previous_failure = self.server.failure_replays.take(self._failure_key)
|
||||
if previous_failure:
|
||||
@@ -547,6 +753,8 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if external or mixed or gecis_context.foreign(req):
|
||||
try:
|
||||
normalized = gecis_context.normalize(req)
|
||||
if external:
|
||||
normalized = gecis_context.portable_reasoning(normalized)
|
||||
except gecis_context.TransferError as e:
|
||||
self._transfer_begin()
|
||||
return self._transfer_fail(str(e))
|
||||
|
||||
@@ -142,6 +142,7 @@ def call(handler, module, slug, source, budget, settings, deadline, depth=0):
|
||||
text = ozet_kopru.sse_metni(bytes(raw))
|
||||
if not text or len(text.strip()) < 80:
|
||||
raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi')
|
||||
gc.require_summary_coverage(chunk, text)
|
||||
results.append(text)
|
||||
combined = '\n\n'.join(results)
|
||||
if not combined:
|
||||
@@ -211,6 +212,8 @@ def prepare(handler, module, payload, checkpoints):
|
||||
protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns'])
|
||||
checkpoints.begin(scope, old, settings)
|
||||
saved = checkpoints.get(scope, old, settings) if old else None
|
||||
if saved and gc.summary_missing(old[:saved[0]], saved[1]):
|
||||
saved = None
|
||||
effective = dict(req, input=protected + [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] + recent) if saved else req
|
||||
if not compact and gc.tokens(effective) <= trigger:
|
||||
return {'request': effective, 'window': window}
|
||||
|
||||
@@ -12,11 +12,52 @@ import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
import ozet_kopru
|
||||
import compression_settings
|
||||
|
||||
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
|
||||
"function_call": "fc", "function_call_output": "fco"}
|
||||
RATIO = 0.70
|
||||
PEER_OMITTED = "[Aktarım notu: Burada okunamayan şifreli bir alt ajan iletisi vardı; aktarılmadı.]"
|
||||
REASONING_OMITTED = "[Aktarım notu: Şifreli model düşünme kaydı aktarılmadı.]"
|
||||
|
||||
|
||||
def summary_missing(source, summary):
|
||||
"""Check literal decisions, file paths and numeric settings, never secrets.
|
||||
|
||||
This is a loss detector for concrete references, not a semantic proof of
|
||||
every sentence. Missing references must not enter a durable checkpoint.
|
||||
"""
|
||||
if isinstance(source, str):
|
||||
try:
|
||||
source = json.loads(source)
|
||||
except ValueError:
|
||||
pass
|
||||
def strings(value):
|
||||
if isinstance(value, str):
|
||||
yield value
|
||||
elif isinstance(value, dict):
|
||||
for child in value.values():
|
||||
yield from strings(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value:
|
||||
yield from strings(child)
|
||||
text = '\n'.join(strings(source)).replace('\\', '/')
|
||||
target = summary.replace('\\', '/').replace('`', '')
|
||||
identifiers = set(re.findall(r'\b[A-Z][A-Z0-9]{0,23}(?:-[A-Z][A-Z0-9]{0,23}){0,3}-\d[A-Za-z0-9_-]{0,63}\b', text))
|
||||
identifiers = {x for x in identifiers if not re.match(r'(?:SECRET|TOKEN|APIKEY|PASSWORD|OTP)-', x)}
|
||||
paths = set(re.findall(r'(?<![A-Za-z0-9_.-])(?:[A-Za-z]:/(?:[A-Za-z0-9_.-]{1,160}/){0,24}|(?:[A-Za-z0-9_.-]{1,160}/){1,24})[A-Za-z0-9_.-]{1,160}\.(?:json|py|tsx?|jsx?|md|txt|ya?ml|toml|ini|sql|csv|log)\b', text)) if '/' in text else set()
|
||||
assignments = set(re.findall(r'\b([A-Za-z_][A-Za-z0-9_]{0,63})\s*=\s*(\d{1,16}|true|false)\b', text))
|
||||
assignments = {(key, value) for key, value in assignments
|
||||
if not re.search(r'key|token|secret|password|sifre|anahtar|otp', key, re.I)}
|
||||
missing = sum(value not in target for value in identifiers | paths)
|
||||
missing += sum(not re.search(re.escape(key) + r'\s*[=:]\s*' + re.escape(value) + r'\b', target)
|
||||
for key, value in assignments)
|
||||
return missing
|
||||
|
||||
|
||||
def require_summary_coverage(source, summary):
|
||||
missing = summary_missing(source, summary)
|
||||
if missing:
|
||||
raise TransferError(f'Özet {missing} karar, dosya yolu veya sayısal ayarı korumadı; eksik özet kabul edilmedi.')
|
||||
|
||||
|
||||
class TransferError(ValueError):
|
||||
@@ -27,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):
|
||||
@@ -56,9 +117,14 @@ def foreign(req):
|
||||
|
||||
def native_opaque(req):
|
||||
"""Native encrypted state belongs to the subscription, even in mixed history."""
|
||||
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content")
|
||||
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET)
|
||||
for item in req.get("input") or [])
|
||||
return any(isinstance(item, dict) and (
|
||||
(item.get("type") in ("compaction", "reasoning") and item.get("encrypted_content")
|
||||
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET))
|
||||
or (item.get("type") == "agent_message" and any(
|
||||
isinstance(part, dict) and part.get("type") == "encrypted_content"
|
||||
for part in item.get("content") or [])))
|
||||
for item in req.get("input") or [])
|
||||
|
||||
|
||||
|
||||
def foreign_history(req):
|
||||
@@ -135,14 +201,10 @@ def portable_reasoning(req):
|
||||
portable = []
|
||||
for part in content:
|
||||
if isinstance(part, dict) and part.get("type") == "encrypted_content":
|
||||
# Codex peer delivery can put the literal task/result in
|
||||
# this wrapper. It is distinct from encrypted reasoning.
|
||||
# External translators otherwise silently drop the task.
|
||||
text = part.get("encrypted_content")
|
||||
if (not isinstance(text, str) or text.startswith("gAAAA")
|
||||
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
|
||||
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
|
||||
portable.append({"type": "input_text", "text": text})
|
||||
# Treat the typed wrapper as opaque regardless of whether
|
||||
# its bytes happen to look like plaintext. Never decrypt,
|
||||
# guess, or forward it to a different provider.
|
||||
portable.append({"type": "input_text", "text": PEER_OMITTED})
|
||||
else:
|
||||
portable.append(part)
|
||||
result.append({"type": "message", "role": "user", "content": [
|
||||
@@ -158,6 +220,8 @@ def portable_reasoning(req):
|
||||
for part in item.get(field) or []:
|
||||
if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]:
|
||||
visible.append(part["text"])
|
||||
if item.get("encrypted_content"):
|
||||
visible.append(REASONING_OMITTED)
|
||||
if visible:
|
||||
# Keep this on the assistant side: a synthetic user message would
|
||||
# change split_history's count of protected user turns.
|
||||
@@ -214,6 +278,8 @@ class Preparer:
|
||||
protected, old, recent = split_history(req.get("input") or [], keep)
|
||||
original_old = old
|
||||
saved = checkpoints.get(scope, old, settings) if checkpoints and old else None
|
||||
if saved and summary_missing(old[:saved[0]], saved[1]):
|
||||
saved = None
|
||||
if saved:
|
||||
old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:]
|
||||
req = dict(req, input=protected + old + recent)
|
||||
@@ -233,8 +299,10 @@ class Preparer:
|
||||
if any(isinstance(x, dict) and x.get("encrypted_content") for x in old):
|
||||
raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.")
|
||||
source = encoded(old)
|
||||
factor = compression_settings.LEVELS[settings['level']] if settings else .5
|
||||
budget = max(512, int(available * factor))
|
||||
# The configured target already bounds the resulting context. Applying
|
||||
# the level's ratio again denied the summarizer most of that budget.
|
||||
# Level controls writing style; the whole available budget stays usable.
|
||||
budget = max(512, available)
|
||||
key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest()
|
||||
with self.lock:
|
||||
summary = self.cache.get(key)
|
||||
@@ -242,6 +310,7 @@ class Preparer:
|
||||
summary = summarize(source, budget)
|
||||
if not isinstance(summary, str) or len(summary.strip()) < 80:
|
||||
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
|
||||
require_summary_coverage(old, summary)
|
||||
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
|
||||
if tokens(candidate) > limit:
|
||||
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -17,7 +17,7 @@ class AgentPayloadTests(unittest.TestCase):
|
||||
],
|
||||
}]}
|
||||
|
||||
def test_actual_peer_wrapper_survives_external_text_extraction(self):
|
||||
def test_typed_encrypted_wrapper_is_omitted_even_if_bytes_look_like_text(self):
|
||||
task = "Run exec_command: Write-Output (157 * 29); report the exit code."
|
||||
request = self.request(task)
|
||||
original = copy.deepcopy(request)
|
||||
@@ -26,7 +26,8 @@ class AgentPayloadTests(unittest.TestCase):
|
||||
# encrypted_content part. Checking the JSON string alone misses loss.
|
||||
text = "\n".join(part["text"] for item in request["input"]
|
||||
for part in item["content"] if part["type"] == "input_text")
|
||||
self.assertIn(task, text)
|
||||
self.assertNotIn(task, text)
|
||||
self.assertIn(gecis_context.PEER_OMITTED, text)
|
||||
self.assertIn('/root/test', text)
|
||||
self.assertEqual(request["input"][0]["role"], "user")
|
||||
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
|
||||
@@ -35,18 +36,22 @@ class AgentPayloadTests(unittest.TestCase):
|
||||
request = self.request("Result: 4553; exit code: 0")
|
||||
original = copy.deepcopy(request)
|
||||
result = gecis_context.portable_reasoning(request)
|
||||
self.assertEqual(result["input"][0]["content"][-1]["text"], "Result: 4553; exit code: 0")
|
||||
self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
|
||||
self.assertEqual(result["input"][0]["content"][1], original["input"][0]["content"][0])
|
||||
self.assertEqual(request, original)
|
||||
|
||||
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self):
|
||||
def test_opaque_peer_payload_is_replaced_by_a_visible_note(self):
|
||||
for opaque in ("gAAAA_native_ciphertext", "A" * 200, None):
|
||||
with self.subTest(opaque=opaque):
|
||||
with self.assertRaises(gecis_context.TransferError):
|
||||
gecis_context.portable_reasoning(self.request(opaque))
|
||||
result = gecis_context.portable_reasoning(self.request(opaque))
|
||||
self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
|
||||
self.assertNotIn("encrypted_content", json.dumps(result))
|
||||
|
||||
def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self):
|
||||
request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]}
|
||||
self.assertEqual(gecis_context.portable_reasoning(request)["input"], [])
|
||||
result = gecis_context.portable_reasoning(request)
|
||||
self.assertNotIn("Run a command now", json.dumps(result))
|
||||
self.assertEqual(result["input"][0]["content"][0]["text"], gecis_context.REASONING_OMITTED)
|
||||
|
||||
|
||||
class PlaintextAgentWireTests(unittest.TestCase):
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -68,7 +68,7 @@ class History(unittest.TestCase):
|
||||
self.assertNotIn("OPAQUE_", json.dumps(result))
|
||||
self.assertEqual(req, before)
|
||||
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 32768))
|
||||
self.assertEqual(result["input"][-1], {"role": "user", "content": "Latest"})
|
||||
self.assertIn({"role": "user", "content": "Latest"}, result["input"])
|
||||
|
||||
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
|
||||
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
|
||||
@@ -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)
|
||||
@@ -259,19 +260,18 @@ class PreparationService(unittest.TestCase):
|
||||
self.assertNotIn("OPAQUE_REASONING", json.dumps(result))
|
||||
self.assertEqual(req, before)
|
||||
|
||||
def test_large_native_return_summarizes_visible_history_without_ciphertext(self):
|
||||
req = {"model": "gpt-6-astra", "input": [
|
||||
{"role": "user", "content": "KARAR-4827 " + "x" * 190000},
|
||||
{"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
|
||||
{"role": "user", "content": "Latest"}]}
|
||||
before = copy.deepcopy(req)
|
||||
result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"]
|
||||
self.assertEqual(result["model"], "gpt-6-astra")
|
||||
self.assertIn("KARAR-4827", json.dumps(result))
|
||||
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 128000))
|
||||
self.assertEqual(req, before)
|
||||
self.assertTrue(self.server.gelen)
|
||||
self.assertTrue(all(b"OPAQUE_NATIVE" not in call["govde"] for call in self.server.gelen))
|
||||
def test_large_native_return_keeps_original_encrypted_execution_state(self):
|
||||
for opaque in ({"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
|
||||
{"type": "agent_message", "content": [
|
||||
{"type": "encrypted_content", "encrypted_content": "OPAQUE_PEER"}]}):
|
||||
req = {"model": "gpt-6-astra", "input": [
|
||||
{"role": "user", "content": "KARAR-4827 " + "x" * 190000},
|
||||
opaque, {"role": "user", "content": "Latest"}]}
|
||||
before = copy.deepcopy(req)
|
||||
result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"]
|
||||
self.assertEqual(result, before)
|
||||
self.assertEqual(req, before)
|
||||
self.assertEqual(self.server.gelen, [])
|
||||
|
||||
def test_external_compaction_uses_external_summary_not_subscription(self):
|
||||
req = {"model": "ide/test", "input": [{"role": "assistant", "content": "KARAR-4827"}, {"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
|
||||
@@ -281,30 +281,38 @@ class PreparationService(unittest.TestCase):
|
||||
|
||||
|
||||
class SocketMigration(fixtures.Ayirici):
|
||||
def test_opaque_peer_transfer_fails_as_http_400_before_sse_or_upstream(self):
|
||||
def test_opaque_peer_transfer_succeeds_without_ciphertext_or_transfer_error(self):
|
||||
req = {"model": "ide/agy-gemini-3.8-flash-high", "prompt_cache_key": "opaque-peer",
|
||||
"input": [{"type": "agent_message", "author": "/root", "recipient": "/root/peer",
|
||||
"content": [{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"},
|
||||
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]}]}
|
||||
"input": [{"role": "user", "content": "USER_DECISION"},
|
||||
{"type": "agent_message", "author": "/root", "recipient": "/root/peer",
|
||||
"content": [{"type": "input_text", "text": "VISIBLE_PEER_HEADER"},
|
||||
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]},
|
||||
{"role": "assistant", "content": "VISIBLE_ANSWER"},
|
||||
{"type": "function_call", "id": "fc_1", "call_id": "c1", "name": "read", "arguments": "{}"},
|
||||
{"type": "function_call_output", "id": "fco_1", "call_id": "c1", "output": "VISIBLE_RESULT"}]}
|
||||
original = copy.deepcopy(req)
|
||||
raw = json.dumps(req).encode()
|
||||
# Identical retries must receive the same permanent error without a
|
||||
# preparation request, a stream heartbeat, or provider inference.
|
||||
for _ in range(2):
|
||||
status, body, headers = self.iste("POST", "/backend-api/codex/responses", raw)
|
||||
self.assertEqual(status, 400)
|
||||
self.assertEqual(headers["Content-Type"].split(";")[0], "application/json")
|
||||
self.assertNotIn("Transfer-Encoding", headers)
|
||||
error = json.loads(body)["error"]
|
||||
self.assertEqual(error["code"], "aiturk_transfer_failed")
|
||||
self.assertEqual(error["type"], "invalid_request_error")
|
||||
self.assertFalse(error["retryable"])
|
||||
self.assertIn("görev eksik gönderilmedi", error["message"])
|
||||
self.assertNotIn(b"response.failed", body)
|
||||
self.assertEqual(req, original)
|
||||
for model in (req["model"], "ide/another-provider"):
|
||||
req["model"] = model
|
||||
status, body, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
|
||||
self.assertEqual(status, 200)
|
||||
self.assertNotIn(b"aiturk_transfer_failed", body)
|
||||
sent = json.loads(self.kopru.gelen[-1]["govde"])
|
||||
wire = json.dumps(sent, ensure_ascii=False)
|
||||
self.assertNotIn("gAAAA_synthetic_opaque", wire)
|
||||
self.assertNotIn("encrypted_content", wire)
|
||||
self.assertIn(gc.PEER_OMITTED, wire)
|
||||
self.assertIn("VISIBLE_PEER_HEADER", wire)
|
||||
for item in original["input"][:1] + original["input"][2:]:
|
||||
self.assertIn(item, sent["input"])
|
||||
self.assertEqual(req["input"], original["input"])
|
||||
self.assertEqual(self.ozgun.gelen, [])
|
||||
self.assertEqual(self.kopru.gelen, [])
|
||||
self.assertEqual(bridge.TRANSFERS, {})
|
||||
# Returning uses the client's original transcript, including opaque peer
|
||||
# state. Projection never rewrites the persisted source or a native turn.
|
||||
req["model"] = "gpt-6-astra"
|
||||
raw = zstd(json.dumps(req).encode())
|
||||
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw, {"Content-Encoding": "zstd"})
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
|
||||
|
||||
def test_native_opaque_peer_history_remains_byte_identical_on_owner_route(self):
|
||||
req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root",
|
||||
@@ -439,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()
|
||||
@@ -0,0 +1,64 @@
|
||||
import copy
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import compression_settings as cs
|
||||
import gecis_context as gc
|
||||
|
||||
|
||||
class SummaryCoverage(unittest.TestCase):
|
||||
def test_long_noise_is_not_misclassified_as_one_giant_identifier(self):
|
||||
gc.require_summary_coverage('A' * 60000 + 'KARAR-4827' + 'B' * 60000,
|
||||
'KARAR-4827: original decision preserved.')
|
||||
|
||||
def test_partial_decision_ledger_is_rejected_and_full_ledger_is_accepted(self):
|
||||
records = [f'DEC-{i:04d}: src/feature_{i:04d}/config.json limit_{i:04d}={30000+i} TEST-QA-{i:04d} NO-DELETE-{i:04d}'
|
||||
for i in range(200)]
|
||||
source = [{'role': 'user', 'content': '\n'.join(records)}]
|
||||
with self.assertRaises(gc.TransferError):
|
||||
gc.require_summary_coverage(source, '\n'.join(records[:95]))
|
||||
gc.require_summary_coverage(source, '\n'.join(records))
|
||||
|
||||
def test_assignment_values_stay_associated_with_their_keys(self):
|
||||
source = [{'content': 'limit_a=123 limit_b=456 src/a.py DEC-1'}]
|
||||
self.assertGreater(gc.summary_missing(source, 'limit_a=456 limit_b=123 src/a.py DEC-1'), 0)
|
||||
gc.require_summary_coverage(source, 'DEC-1: `src/a.py`; limit_a: 123; limit_b: 456')
|
||||
|
||||
def test_secret_named_assignments_are_not_required_in_a_summary(self):
|
||||
gc.require_summary_coverage([{'content': 'api_key=123456 password=7890 otp=1234'}], 'Secrets omitted.')
|
||||
|
||||
def test_target_budget_is_not_multiplied_by_compression_level_again(self):
|
||||
settings = dict(cs.DEFAULTS, keepTurns=1, triggerPercent=60, targetPercent=25, level='fazla')
|
||||
req = {'input': [{'role': 'user', 'content': 'DEC-1 ' + 'x' * 60000},
|
||||
{'role': 'user', 'content': 'Keep this recent turn.'}]}
|
||||
observed = []
|
||||
def summarize(source, budget):
|
||||
observed.append(budget)
|
||||
return 'DEC-1: All accepted decisions, paths, settings and outstanding work are retained. ' * 2
|
||||
prepared = gc.Preparer().prepare(req, 32768, summarize, settings=settings, automatic=True)
|
||||
protected, _, recent = gc.split_history(req['input'], 1)
|
||||
available = gc.input_budget(req, 32768, settings, True) - gc.tokens(dict(req, input=protected + recent)) - 256
|
||||
self.assertEqual(observed, [available])
|
||||
self.assertLessEqual(gc.tokens(prepared), 32768 * .25)
|
||||
|
||||
def test_incomplete_old_checkpoint_is_rebuilt_without_overwriting_source(self):
|
||||
settings = dict(cs.DEFAULTS, keepTurns=1)
|
||||
req = {'input': [{'role': 'user', 'content': 'DEC-1 src/a.py limit_a=123 ' + 'x' * 60000},
|
||||
{'role': 'user', 'content': 'Latest'}]}
|
||||
original = copy.deepcopy(req)
|
||||
with tempfile.TemporaryDirectory() as home:
|
||||
store = cs.Checkpoints(home)
|
||||
old = gc.split_history(req['input'], 1)[1]
|
||||
store.save('scope', old, settings, 'Incomplete old summary without the required details. ' * 3)
|
||||
calls = []
|
||||
def summarize(source, budget):
|
||||
calls.append(1)
|
||||
return 'DEC-1 src/a.py limit_a=123. Accepted decision and all relevant constraints retained. ' * 2
|
||||
result = gc.Preparer().prepare(req, 32768, summarize, 'scope', settings=settings, checkpoints=store)
|
||||
self.assertEqual(calls, [1])
|
||||
self.assertEqual(req, original)
|
||||
gc.require_summary_coverage(old, gc.encoded(result))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -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 = '22'
|
||||
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'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "aiturk-ide",
|
||||
"productName": "AITURK IDE",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta.44",
|
||||
"version": "1.0.0-beta.47",
|
||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||
"author": "AITURK / TurkServis",
|
||||
"repository": {
|
||||
|
||||
@@ -63,7 +63,8 @@ export function CompressionSettingsView() {
|
||||
{draft.budgetMode === 'percent' ? numeric('transferPercent', t('Hedef model bağlamı (%)', 'Target model context (%)'), 10, 90) : numeric('maxTokens', t('Maksimum giriş tokenı', 'Maximum input tokens'), 1024, 2000000)}
|
||||
<ListRow title={t('Otomatik sıkıştırma', 'Automatic compression')} action={<Switch aria-label={t('Otomatik sıkıştırma', 'Automatic compression')} disabled={busy} checked={draft.autoEnabled} onCheckedChange={v => change('autoEnabled', v)} />} />
|
||||
{numeric('triggerPercent', t('Başlama eşiği (%)', 'Trigger threshold (%)'), 20, 90)}
|
||||
{numeric('targetPercent', t('Sıkıştırma sonrası hedef (%)', 'Target after compression (%)'), 10, 80)}
|
||||
{numeric('targetPercent', t('Sıkıştırma sonrası üst sınır (%)', 'Maximum after compression (%)'), 10, 80)}
|
||||
<p className="text-sm text-(--ui-text-secondary)">{t('Bu bir üst sınırdır; tekrarlar azaldığında özet daha küçük olabilir. Sıkıştırma seviyesi anlatım yoğunluğunu belirler, bu sınırı ayrıca küçültmez.', 'This is an upper limit; removing repetition can produce a smaller summary. Compression level controls writing density without shrinking this budget again.')}</p>
|
||||
{numeric('timeoutSeconds', t('Sıkıştırıcı bekleme süresi (saniye)', 'Summarizer timeout (seconds)'), 1, 3600)}
|
||||
<Button disabled={busy || draft.targetPercent >= draft.triggerPercent} onClick={async () => {
|
||||
setBusy(true); setMessage(''); setFailed(false)
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.44",
|
||||
"version": "1.0.0-beta.47",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@assistant-ui/core": "0.2.23",
|
||||
|
||||
Reference in new issue
Block a user