diff --git a/apps/desktop/assets/codex-bridge/README.md b/apps/desktop/assets/codex-bridge/README.md index dcf7b91..17c9051 100644 --- a/apps/desktop/assets/codex-bridge/README.md +++ b/apps/desktop/assets/codex-bridge/README.md @@ -27,6 +27,16 @@ Run: `python bridge.py --home --hermes-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." diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index f7929a8..0ce8e45 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -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 = "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] diff --git a/apps/desktop/assets/codex-bridge/gecis_context.py b/apps/desktop/assets/codex-bridge/gecis_context.py index 3e12e04..a8bce32 100644 --- a/apps/desktop/assets/codex-bridge/gecis_context.py +++ b/apps/desktop/assets/codex-bridge/gecis_context.py @@ -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): diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py index 9135b8a..99fc536 100644 --- a/apps/desktop/assets/codex-bridge/kopru.py +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -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). diff --git a/apps/desktop/assets/codex-bridge/test_denetim_ek.py b/apps/desktop/assets/codex-bridge/test_denetim_ek.py index a6510e0..9cfbf55 100644 --- a/apps/desktop/assets/codex-bridge/test_denetim_ek.py +++ b/apps/desktop/assets/codex-bridge/test_denetim_ek.py @@ -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}) diff --git a/apps/desktop/assets/codex-bridge/test_gecis_context.py b/apps/desktop/assets/codex-bridge/test_gecis_context.py index 3047c6e..3e802bb 100644 --- a/apps/desktop/assets/codex-bridge/test_gecis_context.py +++ b/apps/desktop/assets/codex-bridge/test_gecis_context.py @@ -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() diff --git a/apps/desktop/electron/codex-link-config.ts b/apps/desktop/electron/codex-link-config.ts index 68359d6..b3c39d5 100644 --- a/apps/desktop/electron/codex-link-config.ts +++ b/apps/desktop/electron/codex-link-config.ts @@ -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 = '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' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6eda61f..05e3932 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "aiturk-ide", "productName": "AITURK IDE", "private": true, - "version": "1.0.0-beta.46", + "version": "1.0.0-beta.47", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "author": "AITURK / TurkServis", "repository": { diff --git a/package-lock.json b/package-lock.json index bc708a4..e5aff1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/desktop": { "name": "aiturk-ide", - "version": "1.0.0-beta.46", + "version": "1.0.0-beta.47", "license": "MIT", "dependencies": { "@assistant-ui/core": "0.2.23",