Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69daf809e0 | ||
|
|
f1802299f0 |
No files matched your search
@@ -56,7 +56,7 @@ jobs:
|
||||
- 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'
|
||||
- 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
|
||||
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
|
||||
|
||||
@@ -27,6 +27,28 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
|
||||
|
||||
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."
|
||||
|
||||
@@ -42,7 +42,7 @@ import gecis_context
|
||||
import terminal_stream
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "25" # 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:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
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")
|
||||
|
||||
# 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)
|
||||
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 {}
|
||||
p = bilgi.get("pencere")
|
||||
if p and i < len(sira) - 1 and pencere.bekci_calisir(bilgi.get("kaynak"), p):
|
||||
if tahmin is None:
|
||||
tahmin = pencere.tahmini_token(req, 2.0)
|
||||
tahmin = pencere.tahmini_token(req, gecis_context.bytes_per_token(ad))
|
||||
if tahmin > int(p * 0.90):
|
||||
log(f"{tag} katman {ad} atlandi: istek sigmaz (tahmini {tahmin} token, pencere {p})")
|
||||
continue
|
||||
@@ -1628,7 +1675,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 +1683,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).
|
||||
@@ -1959,7 +2006,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if not hedef_pencere or not pencere.bekci_calisir(pencere_kaynagi, hedef_pencere):
|
||||
hedef_pencere = 32768
|
||||
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)
|
||||
if rewriter is not None:
|
||||
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);
|
||||
# only failures before the work starts (no connection, 500/502/503/529) are retried.
|
||||
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")
|
||||
try:
|
||||
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
|
||||
@@ -2092,6 +2143,27 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
break
|
||||
log(f"{tag} deneme {attempt}: {e}")
|
||||
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.
|
||||
# Other models retain their original response/Jev/stream pipeline.
|
||||
if jev_denetim.izleme_mi(target):
|
||||
|
||||
@@ -33,7 +33,7 @@ class AccessPrograms(unittest.TestCase):
|
||||
"fix_tool_ids": lambda req: 0,
|
||||
"drop_foreign_items": lambda req: 0,
|
||||
"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,
|
||||
}
|
||||
exec(compile(ast.Module(body=taraf + [flatten, claude_sonu, responses, ozgun], type_ignores=[]), "bridge.py", "exec"), self.env)
|
||||
|
||||
@@ -40,7 +40,7 @@ class ComboNotResent(unittest.TestCase):
|
||||
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
|
||||
"SseRewriter": Mock(), "log": lambda message: None,
|
||||
"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"),
|
||||
pick("is_timeout"), pick("kopma_mi"), responses],
|
||||
|
||||
@@ -40,6 +40,11 @@ def method(name):
|
||||
|
||||
|
||||
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)
|
||||
return env
|
||||
|
||||
@@ -483,6 +488,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})
|
||||
|
||||
@@ -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):
|
||||
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()
|
||||
@@ -336,7 +336,7 @@ class TurBasi(Ev):
|
||||
class KatmanPenceresi(unittest.TestCase):
|
||||
def suzgec(self, bilgi):
|
||||
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)
|
||||
return env['sigan_katmanlar'], logs
|
||||
|
||||
|
||||
@@ -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 = '25'
|
||||
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_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.46",
|
||||
"version": "1.0.0-beta.48",
|
||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||
"author": "AITURK / TurkServis",
|
||||
"repository": {
|
||||
|
||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "aiturk-ide",
|
||||
"version": "1.0.0-beta.46",
|
||||
"version": "1.0.0-beta.48",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@assistant-ui/core": "0.2.23",
|
||||
|
||||
Reference in new issue
Block a user