Compare commits

...
Author SHA1 Message Date
yilsemandClaude Opus 5.5 94e4aa2a74 release: AITURK IDE 1.0.0-beta.25 (subscription models kept, summary by subscription model)
ci / mobile-link-windows (push) Successful in 59s
ci / approval-gate-windows (push) Successful in 2m42s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:32:54 +03:00
yilsemandClaude Opus 5.5 adf73eeedd fix(codex-link): summarize with a subscription model, reject tiny summaries, describe added models
Model switch lost the conversation: on 10 Oct a 1 MB thread was compacted by the
newly selected small model into 2201 characters. The compaction summary for a bridge
model is now written by a subscription model (ozet_kopru.ozet_modeli, default
gpt-6.1-sol); if that request is refused before it starts, the target model is used
as before. A summary far shorter than the conversation warrants is not accepted:
Codex gets response.failed and keeps the history.

The line under each added model no longer repeats its name. Teams show their
members, fallback chains their order, single models their provider and the traits
readable from the name; the text fits one phone row (30 characters).

Also normalizes tool call ids in the streamed response so Codex persists ids the
subscription backend accepts (fc_ -> ctc_), and bumps the bridge protocol to 6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:31:53 +03:00
yilsemandClaude Opus 5.5 ce49d6d1cc fix(codex-link): keep subscription models untouched, append bridge models after them
sirala_codex re-sorted the whole list and rewrote every priority, which pushed the
native subscription models to the bottom and put same-named AITURK copies
(GPT-6-Astra) above them. Native models now keep their own order, priority and
name; bridge models are appended after them and a copy that shares a native name
is shown with an (AITURK) suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:16:49 +03:00
yilsem 8ee676d7c6 test(codex-link): track the shipped bridge protocol version
ci / mobile-link-windows (push) Successful in 52s
ci / approval-gate-windows (push) Successful in 1m39s
2026-10-09 23:21:17 +03:00
yilsem 68a97b7489 ci: pin checkout through verified Gitea mirror
ci / mobile-link-windows (push) Failing after 1m24s
ci / approval-gate-windows (push) Successful in 2m11s
2026-10-09 23:18:39 +03:00
yilsem 6e91154f15 fix(codex-link): use member OmniRoute catalog and metadata ordering
ci / mobile-link-windows (push) Failing after 22s
ci / approval-gate-windows (push) Failing after 0s
2026-10-09 23:10:29 +03:00
yilsemandClaude Opus 5.5 88e660768c feat(codex-bridge): keep the gateway's short "9R" mark in Codex model names
ci / mobile-link-windows (push) Successful in 1m5s
ci / approval-gate-windows (push) Successful in 4m11s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 19:27:22 +03:00
yilsemandClaude Opus 5.5 bfd0ff196d feat(codex-bridge): add the models of 9Router's providers to Codex (beta.23)
Codex only received combos and the models named in the IDE config, so none of
the models behind the providers added to 9Router showed up. Those single
models are now always added; 9Router's combos stay hidden. The copy of the
same model that comes through OmniRoute is still added only when the IDE
config names it, so each model is listed once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 19:26:00 +03:00
yilsemandClaude Opus 5.5 1e0f2f9aea feat(codex-bridge): hide 9Router combos in Codex, drop the config backup (beta.22)
ci / mobile-link-windows (push) Successful in 1m12s
ci / approval-gate-windows (push) Successful in 2m39s
Combos that come from 9Router, directly or through the gateway's 9router
provider, are no longer added to Codex's model list; the gateway's own combos
keep their plain names. The IDE no longer leaves a config.toml backup behind:
turning the setting off already restores the file byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 17:51:10 +03:00
14 changed files with 475 additions and 78 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with:
github-server-url: https://gitea.twinpay.one
- name: Mobil TLS, profil ve baslatma regresyonlari
@@ -57,7 +57,7 @@ jobs:
timeout-minutes: 40
steps:
- name: Kaynagi al
uses: actions/checkout@v4
uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with:
github-server-url: https://gitea.twinpay.one
@@ -10,6 +10,13 @@ Codex depends on it while `openai_base_url` points here.
key read from the agent's `.env` on every catalog refresh (never written elsewhere).
- `ide_catalog.py`: which IDE models and combos are added to Codex's model list and
how they are named (`provider/model`, combos by bare name).
The public `/models` catalog supplies every OmniRoute chat model; a member does
not need a private local model allowlist. `/router/catalog` supplies only the
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
entries and internal 9Router combos are excluded. Combos sort A–Z first, then
individual models by provider and name, including OpenAI subscription models.
Codex labels the first visible row `isDefault`; a saved model selection remains
unchanged. A failed or empty refresh keeps the last successful catalog.
- `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made
by one provider stays readable after the user switches to another.
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
+65 -19
View File
@@ -26,7 +26,7 @@ from resim_context import resimleri_sadelestir
import ide_catalog
import ozet_kopru
SURUM = "2"
SURUM = "6"
HERE = os.path.dirname(os.path.abspath(__file__))
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
@@ -183,22 +183,18 @@ def load_ide_catalog():
ROUTES[IDE_ROUTE] = {"url": base + "/responses", "key": anahtar, "flatten_tools": True}
clone = (CFG.get("nine_router_catalog") or {}).get("clone_from", "gpt-6-astra")
levels = (CFG.get("nine_router_catalog") or {}).get("reasoning_levels")
# Secicide yalniz "saglayici/model" gorunur. 9Router listesinde ayni adla zaten duran model
# ikinci kez eklenmez (ayni modelin iki satir olmamasi icin).
# Gorunen adlari ayirt et; farkli kimlikteki modeller adlari benziyor diye silinmez.
var_olan = {m.get("display_name", "").lower() for m in extra_models}
for g in girdiler:
slug = IDE_PREFIX + g["id"]
g["kisa"] = ide_catalog.kisa_ad(g["ad"])
# "ad (2)": ayni modelin gecitteki ikinci yolu; taban ad listede varsa o da eklenmez.
girdiler = [g for g in girdiler if re.sub(r" \(\d+\)$", "", g["kisa"]).lower() not in var_olan]
adlar = ide_catalog.adlandir([(g["id"], g["ad"]) for g in girdiler], dolu=var_olan)
for g in girdiler:
slug = IDE_PREFIX + g["id"]
if slug in model_map:
continue
extra_models.append({"slug": slug, "display_name": adlar[g["id"]],
"description": f"AITURK IDE: {g['ad']}", "route": IDE_ROUTE, "target": g["id"],
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels})
"description": g.get("aciklama") or f"AITURK IDE: {g['ad']}",
"route": IDE_ROUTE, "target": g["id"],
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels,
"kind": g["kind"]})
model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1
else:
@@ -431,6 +427,9 @@ class SseRewriter:
inp = args
d["item"] = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": item.get("status", "completed")}
# Normalize before Codex persists the response. Request-only repair
# cannot help when the user later disables the bridge.
fix_tool_ids({"input": [d["item"]]})
return d
if t.startswith("response.function_call_arguments.") and d.get("item_id") in self.custom_ids:
return None # JSON parcalari custom input'a cevrilemez; done olayinda tam metin gidiyor
@@ -442,6 +441,7 @@ class SseRewriter:
except Exception: inp = item.get("arguments", "")
item = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": "completed"}
fix_tool_ids({"input": [item]})
out.append(item)
d["response"]["output"] = out
return d
@@ -683,6 +683,40 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)")
return metin or None
def _ozet_openai(self, n, path, req, slug, ozet_cfg, girdi_bayt):
"""Kopru modeline (ide/, 9r/) gelen sikistirma isteginin ozetini abonelik modeline yazdirir.
Donus: True = yanit istemciye gitti; False = abonelik istegi baslamadan basarisiz oldu
(istemciye hicbir sey yazilmadi, cagiran hedef modelle devam eder). req cagiranin kopyasidir."""
model = ozet_cfg.get("ozet_modeli", "gpt-6.1-sol")
tag = f"[{n} CHATGPT {model} <- {slug}]"
try:
req["model"] = model
fix_tool_ids(req)
atilan = drop_foreign_items(req)
ozet_kopru.ozetleri_ac(req, yabanci_hedef=False)
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
body = json.dumps(req, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} ozet kopru: abonelik ozeti hazirlanamadi ({type(e).__name__}: {e}), hedef model deneniyor")
return False
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"}
hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream"
r = urllib.request.Request(CHATGPT + path, data=body, headers=hdrs, method="POST")
try:
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
except urllib.error.HTTPError as e:
log(f"{tag} ozet kopru: abonelik ozeti reddedildi (HTTP {e.code} {e.read()[:200]!r}), hedef model deneniyor")
return False
except Exception as e:
log(f"{tag} ozet kopru: abonelik ozeti baslatilamadi ({type(e).__name__}), hedef model deneniyor")
return False
log(f"{tag} ozet kopru: sikistirma ozeti abonelik modeline yazdiriliyor (girdi={len(req.get('input', []))} oge, "
f"{girdi_bayt} bayt, {atilan} yabanci oge atildi)")
akis = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return True
def _models(self, resp, tag):
raw = resp.read()
try:
@@ -724,6 +758,8 @@ class H(http.server.BaseHTTPRequestHandler):
if not m.get("base_instructions") and not has_tmpl:
m.update(saved)
models.append(m); changed += 1
if EXTRA_MODELS:
ide_catalog.sirala_codex(models, EXTRA_MODELS)
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}")
@@ -800,8 +836,9 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} CHATGPT {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
"type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}})
girdi_bayt = len(json.dumps(req.get("input") or [], ensure_ascii=False).encode("utf-8"))
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
ozet_akisi = ozet_kopru.OzetAkisi(None, tohum=str(n))
ozet_akisi = ozet_kopru.OzetAkisi(None, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
ob["acilan"] += 1
log(f"[{n} CHATGPT {slug}] ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
except Exception as e:
@@ -819,7 +856,7 @@ class H(http.server.BaseHTTPRequestHandler):
if parcalar: log(f"[{n} CHATGPT {slug}] " + ", ".join(parcalar))
if ozet_akisi is not None:
sonuc = self._passthrough(n, path, body, tagname=f"CHATGPT {slug}", rewritten=True, rewriter=ozet_akisi)
log(f"[{n} CHATGPT {slug}] ozet kopru: ozet {'uretildi, ' + str(len(ozet_akisi.metin)) + ' karakter' if ozet_akisi.metin else 'URETILEMEDI'}")
log(f"[{n} CHATGPT {slug}] ozet kopru: {ozet_akisi.sonuc_notu()}")
return sonuc
return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}",
rewritten=bool(fixed or atilan or degisti))
@@ -829,11 +866,25 @@ class H(http.server.BaseHTTPRequestHandler):
except Exception as e:
ozet_tetik = False; ozet_acik = False
log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}")
girdi_bayt = 0
if ozet_tetik:
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"[{n} {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
"type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}})
girdi_bayt = len(json.dumps(req.get("input") or [], ensure_ascii=False).encode("utf-8"))
if ozet_tetik and ozet_cfg.get("model"):
# Ozeti baglami alabilen baska bir modele yazdirmak icin (ornegin kucuk hedef kendi basina ozetleyemiyorsa).
alt = MODEL_MAP.get(ozet_cfg["model"])
if alt:
route_name, target, flat = alt
elif ozet_tetik and ozet_cfg.get("ozetleyen", "openai") == "openai":
# Ozeti secili kopru modeli degil, abonelikteki guclu model yazar (10 Ekim 2026: 1 MB'lik
# sohbeti kucuk bir model 2201 karaktere indirdi ve model degisince hafiza gitti).
# Abonelik istegi baslamadan reddedilirse asagidaki eski yol (hedef model) calisir.
if self._ozet_openai(n, path, copy.deepcopy(req), slug, ozet_cfg, girdi_bayt):
return
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
via_router = route_name in (catalog.get("route", "9router"), ide_route)
route = ROUTES[route_name]
@@ -847,11 +898,6 @@ class H(http.server.BaseHTTPRequestHandler):
if ob["acilan"] or ob["cozulen"] or ob["okunamayan"]:
log(f"{tag} ozet kopru: kopru ozeti={ob['acilan']}, OpenAI ozeti duz metne cevrildi={ob['cozulen']}, okunamayan={ob['okunamayan']}")
if ozet_tetik:
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"{tag} ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
"type": "invalid_request_error", "code": "astrabridge_ozet_dongusu"}})
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
rewriter = None
@@ -922,9 +968,9 @@ class H(http.server.BaseHTTPRequestHandler):
time.sleep(min(2 * attempt, 8)); continue
# Headers go to the client from here on: a cut stream is never resent.
if ozet_tetik:
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n))
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: ozet {'uretildi, ' + str(len(akis.metin)) + ' karakter' if akis.metin else 'URETILEMEDI'}")
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
return self._stream(resp, resp.status, tag, rewriter)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
+175 -15
View File
@@ -6,8 +6,8 @@ anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazi
Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json
{"enabled": true, "kapsam": "ide"}
Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik).
kapsam "ide" : IDE ayarinda tanimli modeller + adlandirilmis combolar (varsayilan)
kapsam "hepsi" : gecitteki butun sohbet modelleri (1000+; Codex listesi cok buyur)
kapsam "ide" ve "hepsi": gecidin uye katalogundaki OmniRoute sohbet modelleri.
Yerel config'teki model adlari uye katalogunu sinirlamaz.
"""
import json, os, re, urllib.request
@@ -84,23 +84,34 @@ def ide_yapilandirma(agent=AGENT):
return base.rstrip("/"), varsayilan, adlar
DOKUZ_ISARET = "9R · "
AITURK_EKI = " (AITURK)"
def _isaret(ad):
"""Gecidin dogrudan 9Router'dan gelen tekil modele koydugu kisa isaret korunur (kullanici karari,
9 Ekim 2026: "9Router" yazisi uzun, hic yazmamak da sorun; "9R" olarak kisa gorunsun)."""
return DOKUZ_ISARET if (ad or "").startswith(DOKUZ_ISARET) else ""
def kisa_ad(ad):
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b",
"OmniRoute · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash", combo adi oldugu gibi kalir."""
on = _isaret(ad)
ad = (ad or "").split(" · ")[-1].strip()
parcalar = [p for p in ad.split("/") if p]
if len(parcalar) <= 2:
return "/".join(parcalar) or ad
return parcalar[0] + "/" + parcalar[-1]
return on + ("/".join(parcalar) or ad)
return on + parcalar[0] + "/" + parcalar[-1]
def adlandir(girdiler, dolu=()):
"""[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur;
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar."""
kisa = {k: kisa_ad(a) for k, a in girdiler}
tam = {k: (a or "").split(" · ")[-1].strip() for k, a in girdiler}
tam = {k: _isaret(a) + (a or "").split(" · ")[-1].strip() for k, a in girdiler}
sayac = {}
for v in list(kisa.values()) + list(dolu):
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
@@ -114,10 +125,10 @@ def adlandir(girdiler, dolu=()):
return sonuc
def sec(data, tanimli, kapsam="ide"):
"""Gecit katalogundan Codex'e eklenecek girdiler: [{'id','ad'}], sirali ve tekrarsiz.
def sec(data, tanimli=(), kapsam="ide", turler=None):
"""Gecidin uye katalogundan girdiler: [{'id','ad','kind'}], sirali ve tekrarsiz.
Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez."""
tanimli = {a.lower() for a in tanimli if a}
turler = turler or {}
secilen, gorulen = [], set()
for m in data:
mid = m.get("id") or ""
@@ -125,14 +136,146 @@ def sec(data, tanimli, kapsam="ide"):
if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]):
continue
gorunen = ts.get("display_name") or mid
kisa = gorunen.split(" · ")[-1].strip()
combo = "/" not in kisa # saglayici/model bicimi degil: adlandirilmis combo ya da takma ad
if kapsam != "hepsi" and not combo and kisa.lower() not in tanimli and mid.lower() not in tanimli:
kind = turler.get(mid) or ts.get("kind") or "model"
# Tek uye yolu: 9Router -> OmniRoute -> TurkServis -> Codex.
# Dogrudan 9Router satirlari ayni modelin ikinci kopyasi olarak eklenmez.
if ts.get("source") != "omniroute":
continue
# Combo adi biciminden tahmin edilmez; duz adli Agnes/Bekci modelleri de vardir.
if kind == "combo" and mid.lower().startswith("9router-"):
continue
gorulen.add(mid)
secilen.append({"id": mid, "ad": gorunen, "sira": ts.get("order") or 0})
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0})
secilen.sort(key=lambda s: (s["sira"], s["id"]))
return [{"id": s["id"], "ad": s["ad"]} for s in secilen]
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
for s in secilen:
taban = re.sub(r" \(\d+\)$", "", s["ad"])
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
adlar.add(taban.split(" · ")[-1].strip().lower())
s["ad"] = taban
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"]} for s in secilen]
def sirala_codex(modeller, ekler):
"""Ozgun abonelik modelleri oldugu gibi kalir: sirasi, priority degeri ve adi degismez,
listenin basinda durur. Ek modeller onlarin ARKASINA eklenir: combo A-Z, sonra
saglayici A-Z / model A-Z. Ozgun bir modelle ayni adi tasiyan ek model " (AITURK)"
ekiyle ayirt edilir; secicide ozgunun yerine gecmis gibi gorunmez.
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz."""
bilgiler = {m["slug"]: m for m in ekler}
ozgunler = [m for m in modeller if m.get("slug", "") not in bilgiler]
ek_modeller = [m for m in modeller if m.get("slug", "") in bilgiler]
def yalin(ad): # "GPT-6-1-Sol" ile "GPT-6.1-Sol" ayni ad sayilir
return re.sub(r"[^0-9a-z]+", "", (ad or "").casefold())
ozgun_adlar = {yalin(m.get("display_name") or m.get("slug", "")) for m in ozgunler}
def anahtar(m):
slug = m.get("slug", "")
bilgi = bilgiler.get(slug, {})
ad = kisa_ad(m.get("display_name") or slug)
if ad.startswith(DOKUZ_ISARET):
ad = ad[len(DOKUZ_ISARET):]
if bilgi.get("kind") == "combo":
return (0, ad.casefold(), "", slug)
if slug.startswith("ide/"):
mid = slug[len("ide/"):]
if mid.startswith("9router-"):
mid = mid[len("9router-"):]
saglayici, _, model = ad.partition("/")
if not model:
saglayici, _, _ = mid.partition("-")
model = ad
else:
saglayici, model = "OpenAI", ad
return (1, saglayici.casefold(), model.casefold(), slug)
ek_modeller.sort(key=anahtar)
taban = max([m.get("priority") or 0 for m in ozgunler], default=-1) + 1
for n, model in enumerate(ek_modeller):
model["priority"] = taban + n
ad = model.get("display_name") or model.get("slug", "")
if yalin(ad) in ozgun_adlar:
model["display_name"] = f"{ad}{AITURK_EKI}"
modeller[:] = ozgunler + ek_modeller
# ---- Secicide model adinin altindaki satir ----
# Telefonda bu satir tek satirdir ve yaklasik 30 karakterde kesilir (kullanici ekran goruntusu,
# 10 Ekim 2026); en ayirt edici bilgi basa yazilir, ad tekrar edilmez.
ACIKLAMA_SINIRI = 30
_AILE = (("opus", "opus"), ("sonnet", "sonnet"), ("haiku", "haiku"), ("astra", "astra"), ("terra", "terra"),
("luna", "luna"), ("sol", "sol"), ("gemini", "gemini"), ("qwen", "qwen"), ("glm", "glm"),
("abliterated", "abliterated"), ("deepseek", "deepseek"), ("llama", "llama"), ("gemma", "gemma"))
_SAGLAYICI = {"cc": "Claude", "cx": "ChatGPT", "ag": "Antigravity", "nano": "NanoGPT", "hf": "HuggingFace",
"huggingface": "HuggingFace", "agnes": "Agnes", "bekci": "Bekçi"}
_SANSURSUZ = re.compile(r"uncensored|abliterat|obliterat|derestrict|heretic|unshackled", re.I)
def kisa_model(ad):
""""cc/claude-opus-5-5" -> "opus", "nano/z-ai/glm-5.3-flash-uncensored" -> "glm"; bilinmeyen ad "?"."""
if not ad:
return "?"
son = ad.split("/")[-1].split(":")[0].lower()
parcalar = [p for p in re.split(r"[^a-z]+", son) if p]
for anahtar, kisa in _AILE:
if anahtar in parcalar:
return kisa
return (parcalar[0] if parcalar else son) or "?"
def _yapraklar(dugum, derinlik=0):
"""Sirali (priority) zinciri duz model listesine acar; ic takim kendi uyeleriyle yer alir."""
if not isinstance(dugum, dict) or derinlik > 6:
return ["?"]
if dugum.get("kind") != "combo":
return [kisa_model(dugum.get("model"))]
cikti = []
for uye in dugum.get("members") or []:
cikti.extend(_yapraklar(uye, derinlik + 1))
return cikti or ["?"]
def _sigdir(onek, adlar, ayrac):
"""Sinira sigdigi kadar ad yazar, kalani "+N" ile sayar."""
tekil = [a for i, a in enumerate(adlar) if i == 0 or a != adlar[i - 1]]
for adet in range(len(tekil), 0, -1):
kalan = len(tekil) - adet
metin = onek + ayrac.join(tekil[:adet]) + (f" +{kalan}" if kalan else "")
if len(metin) <= ACIKLAMA_SINIRI or adet == 1:
return metin
return onek.rstrip(": ")
def aciklama(kimlik, ad, tur, kayit=None):
"""Model adinin altinda gorunecek kisa satir. kayit: gecidin /router/catalog girdisi (yoksa None).
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri."""
if tur == "combo":
topoloji = (kayit or {}).get("topology") or {}
uyeler = topoloji.get("members") or []
if topoloji.get("strategy") == "round-robin":
return f"Dönüşümlü: {len(uyeler)} üye"
takim = topoloji if topoloji.get("strategy") == "fusion" else None
if takim is None and uyeler and isinstance(uyeler[0], dict) and uyeler[0].get("strategy") == "fusion":
takim = uyeler[0]
if takim is not None:
return _sigdir("Takım: ", [kisa_model(u.get("model")) for u in takim.get("members") or []], "+")
if uyeler:
return _sigdir("Sıralı: ", _yapraklar(topoloji), ">")
return "Takım"
kisa = kisa_ad(ad)
if kisa.startswith(DOKUZ_ISARET):
kisa = kisa[len(DOKUZ_ISARET):]
kod = kisa.split("/")[0].lower() if "/" in kisa else (kimlik or "").split("-")[0].lower()
parcalar = [_SAGLAYICI.get(kod, kod) or "Tek model"]
if _SANSURSUZ.search(kisa) or _SANSURSUZ.search(kimlik or ""):
parcalar.append("sansürsüz")
if "thinking" in (kisa + " " + (kimlik or "")).lower():
parcalar.append("düşünen")
if len(parcalar) == 1:
parcalar.insert(0, "Tek model")
return " · ".join(parcalar)[:ACIKLAMA_SINIRI]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
@@ -146,5 +289,22 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
return "anahtar-yok", "", "", []
base, varsayilan, adlar = ide_yapilandirma()
req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
data = json.load(urllib.request.urlopen(req, timeout=timeout)).get("data", [])
return "tamam", base, anahtar, sec(data, adlar + [varsayilan], ayar["kapsam"])
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.load(resp).get("data")
if not isinstance(data, list) or not data:
raise ValueError("Gecit bos veya gecersiz model katalogu dondurdu")
req = urllib.request.Request(base + "/router/catalog", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
with urllib.request.urlopen(req, timeout=timeout) as resp:
metadata = json.load(resp).get("models")
if not isinstance(metadata, list) or not metadata:
raise ValueError("Gecit model turlerini dondurmedi")
# Bu ucta uye listesinde olmayan ic kayitlar da bulunur. Yalniz tur bilgisi alinir;
# modele erisim listesi her zaman /models yanitidir.
turler = {m["id"]: m["kind"] for m in metadata if m.get("id") and m.get("kind") in ("combo", "model")}
girdiler = sec(data, adlar + [varsayilan], ayar["kapsam"], turler)
if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
for g in girdiler:
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]))
return "tamam", base, anahtar, girdiler
+32 -1
View File
@@ -45,6 +45,20 @@ OKUNAMADI_NOTU = ("(Kopru notu: Bu noktadan onceki konusma, onceki modelin sifre
"aktarilamadi. Onceki kararlari bilmiyorsan uydurma; kullaniciya sor.)")
KISA_MESAJI = ("AstraBridge: Sohbet ozeti cok kisa cikti (%d karakter, en az %d bekleniyordu) ve gecmisin yerine "
"konmadi; mesajlariniz sohbette duruyor. Tekrar deneyin ya da daha guclu bir model secin.")
def en_az_karakter(girdi_bayt, ayar=None):
"""Bu buyuklukteki bir konusmanin ozeti en az kac karakter olmali.
Olcu: 10 Ekim 2026'da 1.018.151 baytlik sohbet 2201 karaktere indi ve hafiza kayboldu.
Kucuk sohbetlerde esik kuculur ki kisa ama dogru ozetler reddedilmesin."""
ayar = ayar or {}
tavan = int(ayar.get("en_az_tavan", 4000))
oran = max(1, int(ayar.get("en_az_oran", 250))) # her 250 bayt girdi icin 1 karakter
return max(0, min(tavan, int(girdi_bayt) // oran))
def tetik_mi(req):
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
ogeler = req.get("input")
@@ -129,9 +143,11 @@ class OzetAkisi:
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
ic: once calisan cevirici (SseRewriter). Metin gelmezse compaction ogesi uretmez (Codex hatayi gosterir)."""
def __init__(self, ic=None, tohum=""):
def __init__(self, ic=None, tohum="", en_az=0):
self.ic = ic
self.tohum = tohum
self.en_az = en_az # bundan kisa ozet kabul edilmez (bkz. en_az_karakter)
self.kisa = False
self.buf = b""
self.parcalar = {} # item_id -> delta metni
self.bitenler = [] # tamamlanan mesaj metinleri
@@ -171,6 +187,13 @@ class OzetAkisi:
self.bitti = True
if not self.metin:
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
if len(self.metin) < self.en_az:
# Anormal kisa ozet gecmisin yerine konmaz: Codex hatayi gosterir, sohbet oldugu gibi kalir.
self.kisa = True
yanit = dict(d.get("response") or {})
yanit["status"] = "failed"; yanit["output"] = []
yanit["error"] = {"code": "astrabridge_ozet_kisa", "message": KISA_MESAJI % (len(self.metin), self.en_az)}
return _olay({"type": "response.failed", "response": yanit})
oge = ozet_ogesi(self.metin, self.tohum)
yanit = dict(d.get("response") or {})
yanit["output"] = [oge]
@@ -180,6 +203,14 @@ class OzetAkisi:
return _olay(d)
return b""
def sonuc_notu(self):
"""Gunluk satiri icin sonuc (icerik yazilmaz)."""
if not self.metin:
return "ozet URETILEMEDI"
if self.kisa:
return f"ozet REDDEDILDI: {len(self.metin)} karakter, en az {self.en_az} bekleniyordu; gecmis korunuyor"
return f"ozet uretildi, {len(self.metin)} karakter"
def feed(self, chunk):
if self.ic:
chunk = self.ic.feed(chunk)
@@ -2,8 +2,8 @@ import json, os, tempfile, unittest
import ide_catalog as c
def girdi(mid, ad, yetenek=("chat",), sira=0):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira}}
def girdi(mid, ad, yetenek=("chat",), sira=0, kaynak="omniroute", kind="model"):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira, "source": kaynak, "kind": kind}}
KATALOG = [
@@ -11,8 +11,8 @@ KATALOG = [
girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2),
girdi("9router-nano-x", "OmniRoute · nano/x", sira=1),
girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9, kind="combo"),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8, kind="combo"),
girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"),
]
@@ -52,14 +52,39 @@ class Yapilandirma(unittest.TestCase):
class Secim(unittest.TestCase):
def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Tanimli model + adlandirilmis combolar; tanimsiz saglayici modeli ve gorsel modeli yok.
self.assertEqual([x["id"] for x in s], ["9router-cc-opus", "Ajan-Arac", "9router-Takim-Agir-6e65dba5", "9router-Takim-Agir"])
# Uye katalogu yerel config'teki adlarla sinirlanmaz.
self.assertEqual([x["id"] for x in s], ["9router-nano-x", "9router-cc-opus", "Ajan-Arac"])
def test_9router_combolari_gosterilmez(self):
katalog = [girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=1),
girdi("9router-Takim-Agir", "Takim-Agir (2)", sira=2, kind="combo"),
girdi("Takim-Agir", "Takim-Agir (3)", sira=3),
girdi("Ajan-Arac", "Ajan-Arac", sira=4),
girdi("9router-nano-x", "nano/x", kaynak="9router", sira=5)]
s = c.sec(katalog, ["nano/x"], "ide")
# 9Router combosu ve gecidin 9router saglayicisindaki kopyasi yok; OmniRoute'un kendi combosu
# sayacsiz adiyla kalir. Dogrudan 9Router tekilleri de eklenmez.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("Takim-Agir", "Takim-Agir"), ("Ajan-Arac", "Ajan-Arac")])
def test_9router_modelleri_yalniz_omniroute_uzerinden_eklenir(self):
katalog = [girdi("9router-nano-x-aaaa1111", "nano/x", kaynak="9router", sira=1),
girdi("9router-nano-x", "nano/x (2)", sira=2),
girdi("9router-ocg-y-bbbb2222", "ocg/y", kaynak="9router", sira=3),
girdi("vag-z", "vag/z", sira=4),
girdi("9router-ses", "nano/ses", ("tts",), kaynak="9router", sira=5),
girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=6)]
s = c.sec(katalog, [], "ide")
# Yerel model tanimi gerekmez; iki kaynak yerine yalniz OmniRoute yolu gelir.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("9router-nano-x", "nano/x"), ("vag-z", "vag/z")])
def test_gorunen_ad_yalniz_saglayici_ve_model(self):
for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"),
("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"),
("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"),
("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"),
("9R · nano/qwen/qwen3.8-27b", "9R · nano/qwen3.8-27b"), ("9R · ag/gemini", "9R · ag/gemini"),
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
self.assertEqual(c.kisa_ad(tam), beklenen, tam)
@@ -74,7 +99,7 @@ class Secim(unittest.TestCase):
def test_hepsi_kapsami_yalniz_sohbet(self):
s = c.sec(KATALOG, [], "hepsi")
self.assertEqual(len(s), 5)
self.assertEqual(len(s), 3) # iki 9Router combosu hicbir kapsamda gosterilmez
self.assertNotIn("gorsel", [x["id"] for x in s])
@@ -0,0 +1,110 @@
"""The shipped IDE must use the same public OmniRoute catalog as every member."""
import io
import json
import unittest
from unittest.mock import patch
import ide_catalog as c
def row(mid, name, source="omniroute", capabilities=("chat",)):
return {"id": mid, "turkservis": {"display_name": name, "source": source,
"capabilities": list(capabilities)}}
class MemberCatalog(unittest.TestCase):
def test_metadata_classifies_only_public_models(self):
public = [row("plain", "Plain"), row("team", "Team")]
metadata = [{"id": "plain", "kind": "model"}, {"id": "team", "kind": "combo"},
{"id": "internal", "kind": "combo"}]
replies = [io.BytesIO(json.dumps({"data": public}).encode()),
io.BytesIO(json.dumps({"models": metadata}).encode())]
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
patch.object(c, "anahtar_oku", return_value="test-key"), \
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
patch.object(c.urllib.request, "urlopen", side_effect=replies):
status, _, _, selected = c.yukle()
self.assertEqual(status, "tamam")
self.assertEqual({m["id"]: m["kind"] for m in selected}, {"plain": "model", "team": "combo"})
def test_empty_catalog_is_a_refresh_failure(self):
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
patch.object(c, "anahtar_oku", return_value="test-key"), \
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
patch.object(c.urllib.request, "urlopen", return_value=io.BytesIO(b'{"data": []}')):
with self.assertRaises(ValueError):
c.yukle()
def test_public_omniroute_models_do_not_need_private_local_configuration(self):
rows = [row("9router-nano-x", "9R · nano/x"), row("agnes-flat", "Agnes Flat"),
row("Ajan-Arac", "Ajan-Arac"), row("image", "image", capabilities=("image",))]
self.assertEqual({m["id"] for m in c.sec(rows, [])},
{"9router-nano-x", "agnes-flat", "Ajan-Arac"})
def test_direct_route_is_never_added_beside_omniroute(self):
rows = [row("9router-nano-x-direct", "9R · nano/x", "9router"),
row("9router-nano-x", "9R · nano/x")]
self.assertEqual([m["id"] for m in c.sec(rows, ["nano/x"], "hepsi")], ["9router-nano-x"])
def test_model_order_uses_combo_metadata_and_preserves_every_model(self):
models = [{"slug": "gpt-z", "display_name": "GPT Z", "priority": 1},
{"slug": "ide/bekci-kurnaz", "display_name": "kurnaz", "priority": 2},
{"slug": "ide/Z-Team", "display_name": "Z-Team", "priority": 5},
{"slug": "ide/9router-cc-opus", "display_name": "9R · cc/opus", "priority": 3},
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 4}]
extras = [{"slug": m["slug"], "kind": "combo" if m["slug"].endswith("Team") else "model"}
for m in models if m["slug"].startswith("ide/")]
c.sirala_codex(models, extras)
self.assertEqual([m["slug"] for m in models],
["gpt-z", "ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus"])
self.assertEqual([m["priority"] for m in models], [1, 2, 3, 4, 5])
def test_subscription_models_keep_their_own_order_priority_and_name(self):
models = [{"slug": "ide/GPT-6-Astra", "display_name": "GPT-6-Astra", "priority": 3},
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2},
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 0},
{"slug": "gpt-6.1-sol", "display_name": "GPT-6.1-Sol", "priority": 1},
{"slug": "codex-auto-review", "display_name": "Codex Auto Review", "priority": 43}]
models.append({"slug": "ide/GPT-6-1-Sol", "display_name": "GPT-6-1-Sol", "priority": 2})
extras = [{"slug": "ide/GPT-6-Astra", "kind": "combo"}, {"slug": "ide/A-Team", "kind": "combo"},
{"slug": "ide/GPT-6-1-Sol", "kind": "combo"}]
originals = [dict(m) for m in models if not m["slug"].startswith("ide/")]
c.sirala_codex(models, extras)
self.assertEqual([m for m in models if not m["slug"].startswith("ide/")], originals)
self.assertEqual([m["slug"] for m in models],
["gpt-6-astra", "gpt-6.1-sol", "codex-auto-review", "ide/A-Team", "ide/GPT-6-1-Sol",
"ide/GPT-6-Astra"])
self.assertEqual([m["priority"] for m in models[3:]], [44, 45, 46])
self.assertEqual([m["display_name"] for m in models[4:]], ["GPT-6-1-Sol (AITURK)", "GPT-6-Astra (AITURK)"])
def test_description_line_tells_what_the_entry_is_and_fits_a_phone_row(self):
yedek = {"kind": "combo", "model": "Yedek", "strategy": "priority", "members": [
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
{"kind": "model", "model": "nano/abliteration-ai/abliterated-model-large-v2"}]}
takim = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "combo", "model": "Takim-Hizli", "strategy": "fusion", "members": [
{"kind": "model", "model": "ag/gemini-3.8-flash-high"}, {"kind": "model", "model": "cc/claude-sonnet-5-5"}]},
yedek]}}
self.assertEqual(c.aciklama("Takim-Hizli", "Takim-Hizli", "combo", takim), "Takım: gemini+sonnet")
sirali = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "cx/gpt-6-astra"}, {"kind": "model", "model": "cc/claude-opus-5-5"},
{"kind": "model", "model": "cc/claude-sonnet-5-5"}, {"kind": "model", "model": "ag/gemini-3.8-flash-high"}]}}
self.assertEqual(c.aciklama("Ajan-Arac", "Ajan-Arac", "combo", sirali), "Sıralı: astra>opus>sonnet +1")
# An unresolved first member stays visible instead of being hidden behind the fallback.
eksik = {"topology": {"kind": "combo", "strategy": "priority", "members": [{"kind": "combo", "model": None}, yedek]}}
self.assertEqual(c.aciklama("GPT-6-Sol", "GPT-6-Sol", "combo", eksik), "Sıralı: ?>glm>abliterated")
donen = {"topology": {"kind": "combo", "strategy": "round-robin", "members": [yedek, yedek, yedek]}}
self.assertEqual(c.aciklama("Takim-Sira", "Takim-Sira", "combo", donen), "Dönüşümlü: 3 üye")
self.assertEqual(c.aciklama("X", "X", "combo", None), "Takım")
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"), "Tek model · Claude")
self.assertEqual(c.aciklama("9router-nano-qwen-qwen3.8-27b-uncensored-thinking",
"9R · nano/qwen3.8-27b-uncensored:thinking", "model"),
"NanoGPT · sansürsüz · düşünen")
self.assertEqual(c.aciklama("bekci-kurnaz", "kurnaz", "model"), "Tek model · Bekçi")
uzun = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": f"x/model{chr(97 + i) * 9}"} for i in range(8)]}}
self.assertLessEqual(len(c.aciklama("U", "U", "combo", uzun)), c.ACIKLAMA_SINIRI)
for ad in ("Takim-Hizli", "9R · cc/claude-opus-5-5"):
self.assertNotIn(ad, c.aciklama(ad, ad, "combo" if "Takim" in ad else "model", takim))
if __name__ == "__main__":
unittest.main()
@@ -62,6 +62,27 @@ class ModelSwitch(unittest.TestCase):
self.assertEqual(rewritten["item"]["type"], "custom_tool_call")
self.assertEqual(rewritten["item"]["input"], "remember-4827")
self.assertEqual(rewritten["item"]["call_id"], "call_4827")
self.assertEqual(rewritten["item"]["id"], "ctc_chatcmpl-probe")
def test_custom_stream_ids_are_valid_before_history_is_saved(self):
# Real failure after disabling the bridge and returning to OpenAI:
# input[32].id fc_call_... was saved as a custom_tool_call by Codex.
item = {"type": "function_call", "id": "fc_call_3f427ecc539d4c779c1252077701216f",
"call_id": "call_original", "name": "probe",
"arguments": json.dumps({"input": "keep this exact tool input"})}
adapter = env["SseRewriter"]({"probe"})
converted = []
for kind in ("response.output_item.added", "response.output_item.done"):
converted.append(adapter._fix({"type": kind, "item": dict(item)})["item"])
final = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})
converted.append(final["response"]["output"][0])
for result in converted:
self.assertEqual(result["type"], "custom_tool_call")
self.assertEqual(result["id"], "ctc_call_3f427ecc539d4c779c1252077701216f")
self.assertEqual(result["call_id"], "call_original")
self.assertEqual(result["input"], "keep this exact tool input")
history = {"input": converted}
self.assertEqual(env["fix_tool_ids"](history), 0)
if __name__ == "__main__":
@@ -109,6 +109,31 @@ class Akis(unittest.TestCase):
a = o.OzetAkisi()
self.assertEqual(o.ozet_metni(coz(a.feed(ham))[1]["item"]), "ozet metni")
def test_anormal_kisa_ozet_gecmisin_yerine_konmaz(self):
# 10 Ekim 2026: 1.018.151 baytlik sohbet 2201 karaktere indi; bu ozet kabul edilmemeli.
self.assertEqual(o.en_az_karakter(1018151), 4000)
self.assertEqual(o.en_az_karakter(100000), 400)
self.assertEqual(o.en_az_karakter(0), 0)
self.assertEqual(o.en_az_karakter(1018151, {"en_az_tavan": 1500}), 1500)
ham = sse({"type": "response.created", "response": {"id": "r1"}},
{"type": "response.output_item.done", "item": self._mesaj("k" * 2201)},
{"type": "response.completed", "response": {"id": "r1", "status": "completed"}})
a = o.OzetAkisi(tohum="7", en_az=o.en_az_karakter(1018151))
olaylar = coz(a.feed(ham) + a.flush())
self.assertEqual([e["type"] for e in olaylar], ["response.created", "response.failed"])
self.assertNotIn("compaction", json.dumps(olaylar))
self.assertEqual(olaylar[1]["response"]["error"]["code"], "astrabridge_ozet_kisa")
self.assertTrue(a.kisa)
self.assertIn("REDDEDILDI", a.sonuc_notu())
# Yeterli uzunlukta ozet ayni esikle kabul edilir; kucuk sohbetin kisa ozeti de reddedilmez.
for metin, esik in (("u" * 4000, o.en_az_karakter(1018151)), ("kisa ama dogru ozet", o.en_az_karakter(3000))):
b = o.OzetAkisi(tohum="8", en_az=esik)
ham2 = sse({"type": "response.output_item.done", "item": self._mesaj(metin)},
{"type": "response.completed", "response": {"id": "r2"}})
cikti = coz(b.feed(ham2))
self.assertEqual(o.ozet_metni(cikti[0]["item"]), metin)
self.assertFalse(b.kisa)
def test_metin_yoksa_compaction_uretilmez_hata_gorunur(self):
ham = sse({"type": "response.created", "response": {}},
{"type": "response.output_item.done", "item": {"type": "function_call", "name": "x"}},
+1 -2
View File
@@ -27,10 +27,9 @@ 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 = '2'
export const CODEX_BRIDGE_VERSION = '6'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_BACKUP_SUFFIX = '.aiturk-yedek'
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
export const CODEX_LINK_RUN_VALUE = 'AITURK-IDE-Codex-Baglama'
export const CODEX_LINK_LAUNCHER_NAME = 'baslat.pyw'
+3 -21
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link'
import {
CODEX_BRIDGE_VERSION,
CODEX_LINK_LINE,
CODEX_LINK_REFRESH_URL,
CODEX_LINK_STATUS_URL,
@@ -21,8 +22,7 @@ const PATHS: CodexLinkPaths = {
}
const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json`
const BACKUP = `${PATHS.codexConfig}.aiturk-yedek`
const STATUS_OK = { ok: true, surum: '2', ide_durum: 'tamam', ide_model: 7, ozet: true }
const STATUS_OK = { ok: true, surum: CODEX_BRIDGE_VERSION, ide_durum: 'tamam', ide_model: 7, ozet: true }
const USER_CONFIG = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n'
interface World {
@@ -73,12 +73,6 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
world.events.push(`write ${file}`)
world.files.set(file, text)
},
backupOnce: (source, target) => {
if (!world.files.has(target)) {
world.events.push(`backup ${target}`)
world.files.set(target, world.files.get(source) ?? '')
}
},
spawnBridge: (command, args, env) => {
world.events.push(`spawn ${command} ${JSON.stringify(args)} ${JSON.stringify(env)}`)
spawned = true
@@ -212,19 +206,17 @@ describe('applyCodexLink', () => {
hermesHome: PATHS.hermesHome
})
// bridge.json → start → verified answer → config.toml (backed up) → login item
// bridge.json → start → verified answer → config.toml → login item
expect(world.events).toEqual([
`write ${BRIDGE_JSON}`,
`spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`,
'status ok',
`backup ${BACKUP}`,
`write ${PATHS.codexConfig}`,
'autostart set'
])
expect(JSON.parse(world.files.get(BRIDGE_JSON) ?? '')).toEqual(codexBridgeConfig())
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
expect(world.files.get(BACKUP)).toBe(USER_CONFIG)
expect(world.autostart).toBe(
`"${PATHS.pythonw}" "${PATHS.bridgeScript}" --home "${PATHS.bridgeHome}" --hermes-home "${PATHS.hermesHome}"`
)
@@ -238,7 +230,6 @@ describe('applyCodexLink', () => {
expect(status).toMatchObject({ enabled: true, bridgeRunning: false, lastError: 'bridge-start-failed' })
expect(spawns(world)).toHaveLength(1)
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.files.has(BACKUP)).toBe(false)
expect(world.autostart).toBeNull()
expect(writes(world)).toEqual([`write ${BRIDGE_JSON}`])
// The start poll is bounded by its one budget.
@@ -289,15 +280,6 @@ describe('applyCodexLink', () => {
expect(spawns(world)).toEqual([])
})
it('never overwrites the first backup', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(BACKUP, 'the original original')
await link.apply()
expect(world.files.get(BACKUP)).toBe('the original original')
})
it('falls back to python.exe when pythonw.exe is missing', async () => {
const { link, world } = createWorld()
world.files.delete(PATHS.pythonw)
-9
View File
@@ -23,7 +23,6 @@ import path from 'node:path'
import {
addCodexLinkLine,
CODEX_BRIDGE_VERSION,
CODEX_LINK_BACKUP_SUFFIX,
CODEX_LINK_LAUNCHER_NAME,
CODEX_LINK_REFRESH_URL,
CODEX_LINK_RUN_KEY,
@@ -67,8 +66,6 @@ export interface CodexLinkDeps {
readText: (file: string) => null | string
/** tmp + rename; creates the parent folder. */
writeTextAtomic: (file: string, text: string) => void
/** Copy `source` to `target` unless `target` already exists. */
backupOnce: (source: string, target: string) => void
/** Start the bridge detached and hidden; it must outlive the IDE. */
spawnBridge: (command: string, args: string[], env: Record<string, string>) => void
/** Parsed JSON body, or null when nothing answered within `budgetMs`. */
@@ -334,7 +331,6 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
if (edit.changed) {
try {
deps.backupOnce(paths.codexConfig, `${paths.codexConfig}${CODEX_LINK_BACKUP_SUFFIX}`)
deps.writeTextAtomic(paths.codexConfig, edit.text)
} catch (error) {
fail('config-write-failed', error)
@@ -472,11 +468,6 @@ export function createNodeCodexLinkDeps(paths: CodexLinkPaths, log: (message: st
exists: file => fs.existsSync(file),
readText: readTextLossless,
writeTextAtomic,
backupOnce: (source, target) => {
if (!fs.existsSync(target)) {
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL)
}
},
spawnBridge: (command, args, env) => {
fs.mkdirSync(paths.bridgeHome, { recursive: true })
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.21",
"version": "1.0.0-beta.25",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.21",
"version": "1.0.0-beta.25",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",