fix(codex-link): stop list re-downloads and count context by measurement
Keep the upstream model-list ETag: chatgpt.com repeats it in every answer (x-models-etag), and the bridge's own stamp made Codex re-download the ~17 MB merged list after every answer (6198 times in one day). Replace the flat 2 bytes/token estimate with measured ratios (673 real requests): Claude family and unknown names 2.5, recognised non-Claude families 3.4. Non-Claude chats were counted about twice as large, were summarized too early and got false "does not fit" errors. Ship IDE beta.47 (bridge version 26). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
This commit is contained in:
1 parent
b347abe1f6
commit
f1802299f0
9 files changed
+84
-10
No files matched your search
@@ -27,6 +27,16 @@ 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."
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -483,6 +483,19 @@ class Liste(unittest.TestCase):
|
||||
self.assertIn(("Content-Encoding", "gzip"), fake.sent)
|
||||
env["save_last_models"].assert_called_once()
|
||||
|
||||
def test_the_list_keeps_the_upstream_stamp_so_codex_does_not_refetch_after_every_answer(self):
|
||||
"""chatgpt.com repeats its list stamp (x-models-etag) in every answer; a changed stamp here made Codex
|
||||
download the whole list again after each answer (6198 times on 20 October 2026)."""
|
||||
env = self.env(EXTRA_MODELS=[{"slug": "ide/a", "display_name": "Aaa"}])
|
||||
fake = Sahte({"chatgpt-account-id": "hesap"}, "GET")
|
||||
resp = types.SimpleNamespace(read=lambda: json.dumps(self.UPSTREAM).encode(),
|
||||
headers={"ETag": 'W/"40b107fc"', "x-models-etag": 'W/"40b107fc"'})
|
||||
env["_models"](fake, resp, "[t]")
|
||||
sent = dict(fake.sent)
|
||||
self.assertEqual((sent["ETag"], sent["x-models-etag"]), ('W/"40b107fc"', 'W/"40b107fc"'))
|
||||
self.assertIn("ide/a", [m["slug"] for m in json.loads(fake.wfile.getvalue())["models"]])
|
||||
self.assertEqual(dict(env["LAST_MODELS"]["hesap"]["headers"])["ETag"], 'W/"40b107fc"')
|
||||
|
||||
def test_no_network_still_serves_the_last_list(self):
|
||||
cached = {"headers": [("Content-Type", "application/json")], "body": b'{"models": []}', "at": "x"}
|
||||
env = self.env(LAST_MODELS={"hesap": cached})
|
||||
|
||||
@@ -195,8 +195,9 @@ class PreparationService(unittest.TestCase):
|
||||
|
||||
def test_output_reserve_can_require_preparation_below_seventy_percent(self):
|
||||
req = {"model": "ide/test", "max_output_tokens": 24000,
|
||||
"input": [{"role": "user", "content": "x" * 20000}, {"role": "user", "content": "latest"}]}
|
||||
"input": [{"role": "user", "content": "x" * 30000}, {"role": "user", "content": "latest"}]}
|
||||
self.assertLess(gc.tokens(req), int(32768 * .7))
|
||||
self.assertGreater(gc.tokens(req), 32768 - 24000)
|
||||
result = self.service._prepare_transfer({"request": req})["request"]
|
||||
self.assertLessEqual(gc.tokens(result), 32768 - 24000)
|
||||
self.assertTrue(self.server.gelen)
|
||||
@@ -446,5 +447,35 @@ class SocketMigration(fixtures.Ayirici):
|
||||
self.assertEqual(len(self.kopru.gelen), 1)
|
||||
|
||||
|
||||
class TokenEstimate(unittest.TestCase):
|
||||
"""Measured bytes/token (20 October 2026): Claude ~2.55, other families >= 3.4. Unknown stays safe."""
|
||||
|
||||
def test_ratio_follows_the_measured_model_family_and_unknown_stays_safe(self):
|
||||
for slug, ratio in (("ide/claude-claude-haiku-5-5", 2.5), ("ide/claude-claude-opus-5-5", 2.5),
|
||||
("ide/nanogpt-Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled", 2.5),
|
||||
("ide/Jail-Omni", 2.5), ("ide/Takim-Hizli", 2.5), ("", 2.5), (None, 2.5),
|
||||
("ide/agy-gemini-3.8-flash-low", 3.4), ("gpt-6.1-sol", 3.4), ("ide/deepseek-deepseek-flash", 3.4),
|
||||
("ide/abliteration-abliterated-model-large-v2", 3.4), ("ide/grok-cli-grok-4.7", 3.4)):
|
||||
self.assertEqual(gc.bytes_per_token(slug), ratio, slug)
|
||||
|
||||
def test_estimate_never_counts_fewer_tokens_than_the_measured_lowest_ratio(self):
|
||||
text = "Türkçe ve English karışık bir metin, kod: def f(x): return x * 2. " * 4000
|
||||
size = len(gc.encoded({"model": "ide/claude-claude-opus-5-5", "input": text}).encode("utf-8"))
|
||||
self.assertGreaterEqual(gc.tokens({"model": "ide/claude-claude-opus-5-5", "input": text}), size / 2.55)
|
||||
size = len(gc.encoded({"model": "ide/agy-gemini-3.8-flash-low", "input": text}).encode("utf-8"))
|
||||
self.assertGreaterEqual(gc.tokens({"model": "ide/agy-gemini-3.8-flash-low", "input": text}), size / 3.41)
|
||||
self.assertGreater(gc.tokens([text]), gc.tokens({"model": "gpt-6.1-sol", "input": text})) # no model: safe
|
||||
|
||||
def test_a_chat_that_really_fits_a_non_claude_target_is_not_refused(self):
|
||||
"""Before: 2 bytes/token counted this ~75k-token chat as ~130k and refused it for a 128k model."""
|
||||
turn = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "soru " + "x" * 64000}]},
|
||||
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "cevap " + "y" * 64000}]}]
|
||||
req = {"model": "ide/agy-gemini-3.8-flash-low", "input": turn * 2}
|
||||
self.assertLess(gc.tokens(req), int(128000 * 0.70))
|
||||
self.assertGreater(len(gc.encoded(req).encode("utf-8")) // 2, int(128000 * 0.70)) # old estimate refused it
|
||||
prepared = gc.Preparer().prepare(req, 128000, lambda text, budget: self.fail("must not summarize"))
|
||||
self.assertEqual(prepared["input"], req["input"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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'
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Generated
+1
-1
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user