Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48103b9ad3 | ||
|
|
615fedde01 | ||
|
|
a46ff51019 | ||
|
|
8a612beba7 | ||
|
|
7c0dc467d5 | ||
|
|
e2793fa38f | ||
|
|
1e2b8e2b9c | ||
|
|
1445e204da | ||
|
|
d9a7661281 | ||
|
|
959f5f1459 | ||
|
|
acf88f8314 | ||
|
|
ac0fdcf355 | ||
|
|
9a54705ed5 | ||
|
|
9739222919 | ||
|
|
94e4aa2a74 | ||
|
|
adf73eeedd | ||
|
|
ce49d6d1cc |
@@ -13,10 +13,10 @@ Codex depends on it while `openai_base_url` points here.
|
|||||||
The public `/models` catalog supplies every OmniRoute chat model; a member does
|
The public `/models` catalog supplies every OmniRoute chat model; a member does
|
||||||
not need a private local model allowlist. `/router/catalog` supplies only the
|
not need a private local model allowlist. `/router/catalog` supplies only the
|
||||||
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
|
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
|
||||||
entries and internal 9Router combos are excluded. Combos sort A–Z first, then
|
entries and internal 9Router combos are excluded. Codex's own subscription models
|
||||||
individual models by provider and name, including OpenAI subscription models.
|
stay first, in their own order and under their own names; added entries follow
|
||||||
Codex labels the first visible row `isDefault`; a saved model selection remains
|
them: combos A–Z, then individual models by provider and name. A saved model
|
||||||
unchanged. A failed or empty refresh keeps the last successful catalog.
|
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
|
- `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.
|
by one provider stays readable after the user switches to another.
|
||||||
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
|
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
|
||||||
@@ -26,3 +26,66 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
|
|||||||
`POST /aiturk/codex-baglama/stop` ends the process.
|
`POST /aiturk/codex-baglama/stop` ends the process.
|
||||||
|
|
||||||
Tests: `python -m unittest discover` in this directory.
|
Tests: `python -m unittest discover` in this directory.
|
||||||
|
|
||||||
|
## Version 8 (IDE beta.27): changing the model by saying so
|
||||||
|
|
||||||
|
Measured on a live voice chat (11 Oct 2026): a model picked on the phone reaches this machine only with a typed
|
||||||
|
message that STARTS a turn; a message that joins a running turn (including the lone `.`) does not carry it.
|
||||||
|
`model_gecis.py` therefore handles the change in the bridge. A short user message such as "modeli gemini flash low
|
||||||
|
yap" (spoken through the voice assistant or typed) is matched against the model list; the chat's requests are then
|
||||||
|
routed to that model (`model-yonlendirme.json`, keyed by `prompt_cache_key`) until Codex sends a different model
|
||||||
|
for the chat, i.e. until the user picks another one in the list.
|
||||||
|
|
||||||
|
- Idle chat: the bridge answers itself ("Sohbetin modeli artık X ..."), which also tells the voice assistant.
|
||||||
|
- Running turn: the command is replaced by a handover note and the SAME turn continues on the new model.
|
||||||
|
- "seçili model hangisi" / "ana model kim" is answered by the bridge with the model actually in use.
|
||||||
|
- An ambiguous name changes nothing and lists the closest names; a name matching no model is not a command.
|
||||||
|
- A lone `.` that joins a running turn is dropped instead of answered (answering it cut the work short).
|
||||||
|
- Handled commands left in the history are turned into notes so a later model does not answer them again.
|
||||||
|
|
||||||
|
Setting: `sesle_model` (default on).
|
||||||
|
|
||||||
|
Audit follow-up (11 Oct 2026, same version number; `GET .../status` now also returns `kod`, the first 8 hex digits
|
||||||
|
of `bridge.py`'s SHA-256, to tell builds apart):
|
||||||
|
|
||||||
|
- Numbers said as words ("opus beş beş", "üç nokta sekiz") and a trailing "ve devam et" no longer drop the command.
|
||||||
|
- Picking the same model again in the list does not clear a spoken route (Codex sends the same name), so the
|
||||||
|
bridge's answer says how to go back. A route that is in use no longer expires after 7 days.
|
||||||
|
- A command is turned into a "model was changed" note only if it took effect; a mid-turn note also reaches a
|
||||||
|
subscription model when hook messages follow it.
|
||||||
|
- Combos: a Claude member selects the stricter token estimate; one unknown member no longer switches the overflow
|
||||||
|
guard off for a known small member; a combo dropped by the peer after the request was sent is not resent.
|
||||||
|
- An old `9r/…` name without a route gets a local 503 instead of going to chatgpt.com; upstream error bodies
|
||||||
|
reach Codex whole, with `Retry-After`.
|
||||||
|
- The last model list is also served when chatgpt.com cannot be reached at all, and is cached when the answer
|
||||||
|
was gzip-compressed. The old `display_map` setting is no longer read (subscription models keep their names).
|
||||||
|
- Log lines carry the date; the access-token tail is no longer logged.
|
||||||
|
|
||||||
|
### Work handoff instruction (prepared, off by default)
|
||||||
|
|
||||||
|
`is_hatti.py`: when `aiturk_hat` is enabled and has a `komut`, the bridge prepends a developer message ("small work
|
||||||
|
yourself; larger work goes to AITURK via this command") to requests whose `originator` header is `Codex Desktop`.
|
||||||
|
The Codex CLI (`codex-tui`, `codex_exec`) never receives it and nothing is written to any file it reads. Each new
|
||||||
|
`originator` value is logged once (`istemci goruldu`).
|
||||||
|
|
||||||
|
## Version 7 (IDE beta.26): external models behave like subscription models
|
||||||
|
|
||||||
|
Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn).
|
||||||
|
|
||||||
|
| Area | Before | Now |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Context window | every added model declared 272 000 | `pencere.py`: NanoGPT's own list for `nano/*`, a local table for `cc`/`cx`/`ag`, a combo declares its smallest member, unknown = 128 000, nothing above 272 000 (the summarizer's window). Codex then compacts by itself when switching to a smaller model. |
|
||||||
|
| Overflow | the target answered with an empty stream; Codex retried five times | `pencere.sigdir` trims the middle of oversized old parts in the outgoing copy; if it still does not fit Codex gets `context_length_exceeded`. A limit reported by the target is learned (`pencere-ogrenilen.json`). |
|
||||||
|
| Tools | clones inherited `code_mode_only` + Responses Lite; GLM and Qwen failed 0/3 on simple tasks (empty answers, reasoning leaked into the answer) | `dis_arac_modu: "direct"` (default): classic tools. GLM 3/3, Qwen 2/3; Claude, `cx` and Gemini unchanged (6/6). `"kalip"` restores the old behaviour. |
|
||||||
|
| Intermediate messages | external assistant messages had no `phase`; Codex treats those as final answers | `SseRewriter` stamps `commentary` / `final_answer`. Deltas are never delayed; only the item's `done` event waits for what follows. |
|
||||||
|
| Interrupted answer | text written before a stop or an app exit was lost | `yarim.py` keeps it (memory only) when the *client* closes the stream and adds it as a note to the next user message of the same conversation, once. |
|
||||||
|
| Images | removed from every old message for all external models | kept for vision-capable targets (user messages, newest first, 1 MB total). |
|
||||||
|
| Summary brake | the 4th summary in 10 minutes failed the turn | three summaries in a row with no normal request in between, or eight in 10 minutes. |
|
||||||
|
| Gateway errors | `event: error` (e.g. `upstream 402`) looked like a cut stream | permanent ones become `response.failed` with a readable message; short upstream bodies are logged. |
|
||||||
|
| Start-up | the model list was fetched before listening; a slow gateway made the IDE give up after 10 s | window data comes from the cache at start and is refreshed in the background; the IDE waits 45 s. |
|
||||||
|
|
||||||
|
Not changed: subscription models, their order and names, and everything on the voice path (`/realtime/*` passes through untouched).
|
||||||
|
|
||||||
|
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
|
||||||
|
|
||||||
|
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> seçildi." (the target is not probed, so it does not say "ready") and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ Yerel config'teki model adlari uye katalogunu sinirlamaz.
|
|||||||
"""
|
"""
|
||||||
import json, os, re, urllib.request
|
import json, os, re, urllib.request
|
||||||
|
|
||||||
|
import pencere
|
||||||
|
|
||||||
# Agent home: HERMES_HOME when the IDE passes it, else the default install location.
|
# Agent home: HERMES_HOME when the IDE passes it, else the default install location.
|
||||||
AGENT = os.environ.get("HERMES_HOME") or os.path.join(os.environ.get("LOCALAPPDATA", ""), "TurkServis", "AITURK-IDE", "agent")
|
AGENT = os.environ.get("HERMES_HOME") or os.path.join(os.environ.get("LOCALAPPDATA", ""), "TurkServis", "AITURK-IDE", "agent")
|
||||||
IDE_DIR = os.path.dirname(os.path.normpath(AGENT))
|
IDE_DIR = os.path.dirname(os.path.normpath(AGENT))
|
||||||
@@ -84,33 +86,61 @@ def ide_yapilandirma(agent=AGENT):
|
|||||||
return base.rstrip("/"), varsayilan, adlar
|
return base.rstrip("/"), varsayilan, adlar
|
||||||
|
|
||||||
|
|
||||||
DOKUZ_ISARET = "9R · "
|
AITURK_EKI = " (AITURK)"
|
||||||
|
# Gecidin kisa saglayici kodu -> kullanicinin gormek istedigi kod (10 Ekim 2026: "agy/gemini").
|
||||||
|
_SAGLAYICI_KISA = {"ag": "agy"}
|
||||||
def _isaret(ad):
|
# Saglayici kodu zaten soyledigi icin model adinin basinda tekrar edilmeyen uretici adi ("cc/opus-5-5").
|
||||||
"""Gecidin dogrudan 9Router'dan gelen tekil modele koydugu kisa isaret korunur (kullanici karari,
|
_TEKRAR = ("claude-",)
|
||||||
9 Ekim 2026: "9Router" yazisi uzun, hic yazmamak da sorun; "9R" olarak kisa gorunsun)."""
|
DUSUNMESIZ = "no-think-"
|
||||||
return DOKUZ_ISARET if (ad or "").startswith(DOKUZ_ISARET) else ""
|
|
||||||
|
|
||||||
|
|
||||||
def kisa_ad(ad):
|
def kisa_ad(ad):
|
||||||
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
|
"""Gecit onekleri ("OmniRoute · ", "9Router · ", "9R · ") ve aradaki saglayici halkalari atilir:
|
||||||
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
|
"nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b", "9R · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash",
|
||||||
saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b",
|
combo adi oldugu gibi kalir. Kullanici karari (10 Ekim 2026): "9R" yazilmaz."""
|
||||||
"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()
|
ad = (ad or "").split(" · ")[-1].strip()
|
||||||
parcalar = [p for p in ad.split("/") if p]
|
parcalar = [p for p in ad.split("/") if p]
|
||||||
if len(parcalar) <= 2:
|
if len(parcalar) <= 2:
|
||||||
return on + ("/".join(parcalar) or ad)
|
return "/".join(parcalar) or ad
|
||||||
return on + parcalar[0] + "/" + parcalar[-1]
|
return parcalar[0] + "/" + parcalar[-1]
|
||||||
|
|
||||||
|
|
||||||
def adlandir(girdiler, dolu=()):
|
def gorunen_ad(kimlik, ad, tur="model"):
|
||||||
|
"""Secicide gorunen ad (kullanici karari, 10 Ekim 2026): combo yalniz kendi adiyla; tek model
|
||||||
|
"saglayici/model" kalibinda, kisa ve surumlu: "9R · cc/claude-opus-5-5" -> "cc/opus-5-5",
|
||||||
|
"9R · ag/gemini-3.8-flash-high" -> "agy/gemini-3.8-flash-high", "Agnes 2.0 Flash" (agnes-agnes-2.0-flash)
|
||||||
|
-> "agnes/agnes-2.0-flash", "kurnaz" (bekci-kurnaz) -> "bekci/kurnaz". Dusunmesiz kopya asil
|
||||||
|
saglayicisiyla gorunur: "no-think/claude-opus-5-5" -> "cc/opus-5-5 (no-think)"."""
|
||||||
|
yalin = kisa_ad(ad)
|
||||||
|
if tur == "combo":
|
||||||
|
return yalin
|
||||||
|
kim, ek = kimlik or "", ""
|
||||||
|
if kim.startswith(DUSUNMESIZ):
|
||||||
|
kim, ek = kim[len(DUSUNMESIZ):], " (no-think)"
|
||||||
|
if kim.startswith("9router-"):
|
||||||
|
kim = kim[len("9router-"):]
|
||||||
|
saglayici, _, model = yalin.partition("/")
|
||||||
|
if not model or ek:
|
||||||
|
kod, _, kalan = kim.partition("-")
|
||||||
|
if not kod or not kalan:
|
||||||
|
return yalin
|
||||||
|
saglayici, model = kod, model or kalan
|
||||||
|
model = model.replace(" ", "-")
|
||||||
|
for tekrar in _TEKRAR:
|
||||||
|
if model.lower().startswith(tekrar) and len(model) > len(tekrar):
|
||||||
|
model = model[len(tekrar):]
|
||||||
|
return f"{_SAGLAYICI_KISA.get(saglayici.lower(), saglayici)}/{model}{ek}"
|
||||||
|
|
||||||
|
|
||||||
|
def adlandir(girdiler, dolu=(), turler=None):
|
||||||
"""[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur;
|
"""[(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."""
|
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar.
|
||||||
kisa = {k: kisa_ad(a) for k, a in girdiler}
|
`turler` ({kimlik: "combo"|"model"}) verilirse adlar gorunen_ad kuraliyla yazilir."""
|
||||||
tam = {k: _isaret(a) + (a or "").split(" · ")[-1].strip() for k, a in girdiler}
|
if turler is None:
|
||||||
|
kisa = {k: kisa_ad(a) for k, a in girdiler}
|
||||||
|
else:
|
||||||
|
kisa = {k: gorunen_ad(k, a, turler.get(k, "model")) for k, a in girdiler}
|
||||||
|
tam = {k: (a or "").split(" · ")[-1].strip() for k, a in girdiler}
|
||||||
sayac = {}
|
sayac = {}
|
||||||
for v in list(kisa.values()) + list(dolu):
|
for v in list(kisa.values()) + list(dolu):
|
||||||
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
|
sayac[v.lower()] = sayac.get(v.lower(), 0) + 1
|
||||||
@@ -118,7 +148,11 @@ def adlandir(girdiler, dolu=()):
|
|||||||
for k, _ in girdiler:
|
for k, _ in girdiler:
|
||||||
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
|
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
|
||||||
if ad.lower() in kullanilan:
|
if ad.lower() in kullanilan:
|
||||||
ad = f"{ad} [{k[-8:]}]"
|
taban = ad
|
||||||
|
for uzunluk in range(8, len(k) + 8, 4): # son 8 karakter de cakisirsa ek uzatilir: iki satir ayni adi tasimaz
|
||||||
|
ad = f"{taban} [{k[-uzunluk:]}]"
|
||||||
|
if ad.lower() not in kullanilan:
|
||||||
|
break
|
||||||
kullanilan.add(ad.lower())
|
kullanilan.add(ad.lower())
|
||||||
sonuc[k] = ad
|
sonuc[k] = ad
|
||||||
return sonuc
|
return sonuc
|
||||||
@@ -144,7 +178,8 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
|
|||||||
if kind == "combo" and mid.lower().startswith("9router-"):
|
if kind == "combo" and mid.lower().startswith("9router-"):
|
||||||
continue
|
continue
|
||||||
gorulen.add(mid)
|
gorulen.add(mid)
|
||||||
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0})
|
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0,
|
||||||
|
"yol": str(m.get("description") or "").split(" · ")[-1].strip()})
|
||||||
secilen.sort(key=lambda s: (s["sira"], s["id"]))
|
secilen.sort(key=lambda s: (s["sira"], s["id"]))
|
||||||
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
|
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
|
||||||
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
|
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
|
||||||
@@ -153,21 +188,29 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
|
|||||||
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
|
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
|
||||||
adlar.add(taban.split(" · ")[-1].strip().lower())
|
adlar.add(taban.split(" · ")[-1].strip().lower())
|
||||||
s["ad"] = taban
|
s["ad"] = taban
|
||||||
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"]} for s in secilen]
|
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen]
|
||||||
|
|
||||||
|
|
||||||
def sirala_codex(modeller, ekler):
|
def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
|
||||||
"""Tum listeyi siralar: combo A-Z, sonra saglayici A-Z / model A-Z.
|
"""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.
|
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz.
|
||||||
Codex'in isDefault etiketi kendi davranisi geregi ilk gorunur satira verilir."""
|
ozgun_kimlikler: ust listeden (chatgpt.com) gelen kimlikler. Ayarda ayni kimlik ek model diye yazilmis
|
||||||
bilgiler = {m["slug"]: m for m in ekler}
|
olsa da o model ozgundur; yerinden ve sirasindan oynamaz."""
|
||||||
|
bilgiler = {m["slug"]: m for m in ekler if m["slug"] not in ozgun_kimlikler}
|
||||||
|
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):
|
def anahtar(m):
|
||||||
slug = m.get("slug", "")
|
slug = m.get("slug", "")
|
||||||
bilgi = bilgiler.get(slug, {})
|
bilgi = bilgiler.get(slug, {})
|
||||||
ad = kisa_ad(m.get("display_name") or 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":
|
if bilgi.get("kind") == "combo":
|
||||||
return (0, ad.casefold(), "", slug)
|
return (0, ad.casefold(), "", slug)
|
||||||
if slug.startswith("ide/"):
|
if slug.startswith("ide/"):
|
||||||
@@ -182,14 +225,104 @@ def sirala_codex(modeller, ekler):
|
|||||||
saglayici, model = "OpenAI", ad
|
saglayici, model = "OpenAI", ad
|
||||||
return (1, saglayici.casefold(), model.casefold(), slug)
|
return (1, saglayici.casefold(), model.casefold(), slug)
|
||||||
|
|
||||||
modeller.sort(key=anahtar)
|
ek_modeller.sort(key=anahtar)
|
||||||
for n, model in enumerate(modeller):
|
taban = max([m.get("priority") or 0 for m in ozgunler], default=-1) + 1
|
||||||
model["priority"] = n
|
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
|
||||||
|
|
||||||
|
|
||||||
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
|
# ---- 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
|
||||||
|
KUCUK_PENCERE = 100_000 # bunun altindaki pencere alt satirda yazilir
|
||||||
|
_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": "Codex", "ag": "Antigravity", "agy": "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, bilgi=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.
|
||||||
|
bilgi (pencere.coz sonucu): Codex'te isi etkileyen iki uyari one yazilir: kucuk baglam ("32K")
|
||||||
|
ve arac cagiramayan model ("araçsız")."""
|
||||||
|
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 = gorunen_ad(kimlik, ad, "model")
|
||||||
|
kod = kisa.split("/")[0].lower() if "/" in kisa else (kimlik or "").split("-")[0].lower()
|
||||||
|
parcalar = [_SAGLAYICI.get(kod, kod) or "Tek model"]
|
||||||
|
if bilgi and bilgi.get("pencere") and bilgi["pencere"] < KUCUK_PENCERE:
|
||||||
|
parcalar.append(pencere.kisa(bilgi["pencere"]))
|
||||||
|
if bilgi and bilgi.get("arac") is False:
|
||||||
|
parcalar.append("araçsız")
|
||||||
|
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")
|
||||||
|
while len(parcalar) > 1 and len(" · ".join(parcalar)) > ACIKLAMA_SINIRI:
|
||||||
|
parcalar.pop() # sona yazilan en az onemli ozellik duser; kelime ortasindan kesilmez
|
||||||
|
return " · ".join(parcalar)[:ACIKLAMA_SINIRI]
|
||||||
|
|
||||||
|
|
||||||
|
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
|
||||||
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
|
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
|
||||||
Ag hatasinda istisna atar; cagiran son listeyi korur."""
|
Ag hatasinda istisna atar; cagiran son listeyi korur. home: koprunun veri klasoru (pencere onbellegi).
|
||||||
|
Her girdi "pencere" (ilan edilecek baglam penceresi), "gorsel" ve "arac" alanlarini tasir."""
|
||||||
ayar = ayar_oku()
|
ayar = ayar_oku()
|
||||||
if not ayar["enabled"]:
|
if not ayar["enabled"]:
|
||||||
return "kapali", "", "", []
|
return "kapali", "", "", []
|
||||||
@@ -213,4 +346,14 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
|
|||||||
girdiler = sec(data, adlar + [varsayilan], ayar["kapsam"], turler)
|
girdiler = sec(data, adlar + [varsayilan], ayar["kapsam"], turler)
|
||||||
if not girdiler:
|
if not girdiler:
|
||||||
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
|
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
|
||||||
|
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
|
||||||
|
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
|
||||||
|
ogr = pencere.ogrenilen(home)
|
||||||
|
aracsiz = pencere.aracsizlar(home)
|
||||||
|
for g in girdiler:
|
||||||
|
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
|
||||||
|
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
|
||||||
|
bilgi["arac"], bilgi["kaynak"])
|
||||||
|
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
|
||||||
|
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]), bilgi)
|
||||||
return "tamam", base, anahtar, girdiler
|
return "tamam", base, anahtar, girdiler
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""Codex IDE'ye "is kucuk degilse AITURK yapacak" talimati (11 Ekim 2026).
|
||||||
|
|
||||||
|
Kullanici karari: Codex Baglama acikken talimat Codex IDE'nin (masaustu uygulamasi) gordugu sistem talimatinda durur;
|
||||||
|
Codex CLI talimati hic gormez, varligindan haberi olmaz. Karari secili model verir; kucuk modellerin yanilmasi kabul
|
||||||
|
edilir, ayri bir yazilimla onune gecilmez.
|
||||||
|
|
||||||
|
Bu yuzden talimat hicbir dosyaya yazilmaz (AGENTS.md'yi CLI da okur): kopru, yalniz masaustu uygulamasindan gelen
|
||||||
|
isteklere, istek gecerken ekler. Istemci `originator` basligindan ayirt edilir (olcum 11 Ekim 2026: masaustu
|
||||||
|
"Codex Desktop", etkilesimli CLI "codex-tui", `codex exec` "codex_exec").
|
||||||
|
|
||||||
|
Hat, Codex Baglama'nin parcasidir: ayri bir dugmesi yoktur, bu bilgisayarda isi gonderebilen bir AITURK komutu
|
||||||
|
(`aiturk is`) bulundugu surece aciktir. Komut yoksa talimat eklenmez: gonderme yolu olmayan bir "AITURK'e gonder"
|
||||||
|
talimati modeli yaniltir. Ayar (bridge.json, istege bagli): "aiturk_hat": {"enabled": false} kapatir;
|
||||||
|
{"komut": "..."} komutu elle verir; {"talimat": "..."} metni degistirir.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
MASAUSTU = {"codex desktop"}
|
||||||
|
ISARET = "[AITURK iş hattı]"
|
||||||
|
VARSAYILAN = (
|
||||||
|
ISARET + " Bu Codex, AITURK'e bağlı. Şu kurala göre çalış:\n"
|
||||||
|
"1. Basit soruyu, bilgi isteğini ya da tek küçük düzeltmeyi kendin yap.\n"
|
||||||
|
"2. Kullanıcı \"sen yap\" derse işi kendin yap.\n"
|
||||||
|
"3. İş küçük değilse (birden çok dosya, birkaç adım, yeni özellik, araştırma gerektiren iş) kendin yapma. "
|
||||||
|
"İşin tam metnini (kullanıcının isteği ve gereken bağlam) UTF-8 bir dosyaya yaz, işi şu komutla AITURK'e gönder, "
|
||||||
|
"bitmesini bekle ve sonucu kullanıcıya kısaca aktar:\n{komut}\n"
|
||||||
|
"Komut \"işi sen yap\" derse ya da hata verirse işi kendin yap. Emin değilsen işi küçük say ve kendin yap."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def masaustu_mu(originator):
|
||||||
|
return str(originator or "").strip().lower() in MASAUSTU
|
||||||
|
|
||||||
|
|
||||||
|
def adaylar(ortam=None):
|
||||||
|
"""Isi gonderebilecek AITURK komutu adaylari, oncelik sirasiyla (kurulu AITURK, PATH, hat deneme kurulumu)."""
|
||||||
|
ortam = os.environ if ortam is None else ortam
|
||||||
|
yerel = ortam.get("LOCALAPPDATA") or ""
|
||||||
|
liste = []
|
||||||
|
if yerel:
|
||||||
|
liste.append(os.path.join(yerel, "TurkServis", "AITURK", "program", "bin", "aiturk.cmd"))
|
||||||
|
bulunan = shutil.which("aiturk")
|
||||||
|
if bulunan:
|
||||||
|
liste.append(bulunan)
|
||||||
|
if yerel:
|
||||||
|
liste.append(os.path.join(yerel, "TurkServis", "AITURK-is-hatti", "Scripts", "aiturk.exe"))
|
||||||
|
return [y for i, y in enumerate(liste) if y and y not in liste[:i]]
|
||||||
|
|
||||||
|
|
||||||
|
def destekliyor(yol, calistir=subprocess.run):
|
||||||
|
"""Bu AITURK `is` komutunu taniyor mu? (Eski surumler tanimaz: "No such command 'is'.")"""
|
||||||
|
if not os.path.isfile(yol):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
sonuc = calistir([yol, "is", "--help"], capture_output=True, timeout=40, cwd=os.path.dirname(yol),
|
||||||
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
||||||
|
return sonuc.returncode == 0
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def komut_bul(ortam=None, destek=destekliyor):
|
||||||
|
"""Isi gonderebilen ilk AITURK komutunun yolu; yoksa None."""
|
||||||
|
for yol in adaylar(ortam):
|
||||||
|
if destek(yol):
|
||||||
|
return yol
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def komut_satiri(yol, sohbet=""):
|
||||||
|
"""Modelin calistiracagi hazir satir. Sohbet kimligi kopruden gelir (sohbetler karismasin); klasor verilmez:
|
||||||
|
komut Codex'in o anki klasorunde calisir, AITURK de isi ayni klasorde yapar."""
|
||||||
|
sohbet = "".join(c for c in str(sohbet or "") if c.isalnum() or c in "-_")
|
||||||
|
return f'"{yol}" is' + (f" --sohbet {sohbet}" if sohbet else "") + ' --dosya "<iş metnini yazdığın dosya>"'
|
||||||
|
|
||||||
|
|
||||||
|
def talimat(ayar, bulunan=None, sohbet=""):
|
||||||
|
"""Eklenecek metin; hat kapaliysa ya da gonderme komutu yoksa None.
|
||||||
|
bulunan: bu bilgisayarda bulunan AITURK komutunun yolu (komut_bul); ayardaki "komut" onun onune gecer."""
|
||||||
|
ayar = ayar if isinstance(ayar, dict) else {}
|
||||||
|
if ayar.get("enabled") is False:
|
||||||
|
return None
|
||||||
|
komut = str(ayar.get("komut") or "").strip() or (komut_satiri(bulunan, sohbet) if bulunan else "")
|
||||||
|
if not komut:
|
||||||
|
return None
|
||||||
|
return str(ayar.get("talimat") or VARSAYILAN).replace("{komut}", komut)
|
||||||
|
|
||||||
|
|
||||||
|
def ekle(req, metin):
|
||||||
|
"""Talimati istegin basina developer mesaji olarak koyar. True: eklendi (zaten varsa eklenmez)."""
|
||||||
|
girdi = req.get("input")
|
||||||
|
if not metin or not isinstance(girdi, list):
|
||||||
|
return False
|
||||||
|
for oge in girdi[:3]:
|
||||||
|
icerik = oge.get("content") if isinstance(oge, dict) else None
|
||||||
|
yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
|
||||||
|
if ISARET in yazi:
|
||||||
|
return False
|
||||||
|
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": metin}]})
|
||||||
|
return True
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026).
|
||||||
|
|
||||||
|
Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla
|
||||||
|
ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu.
|
||||||
|
Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik
|
||||||
|
koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir),
|
||||||
|
kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir.
|
||||||
|
|
||||||
|
- Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ...").
|
||||||
|
- Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer;
|
||||||
|
yarim kalan is kesilmez, yeni model kaldigi yerden devam eder.
|
||||||
|
- Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar.
|
||||||
|
|
||||||
|
Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
DOSYA = "model-yonlendirme.json"
|
||||||
|
OMUR_SN = 7 * 24 * 3600
|
||||||
|
YENILE_SN = 24 * 3600
|
||||||
|
EN_COK_KAYIT = 200
|
||||||
|
EN_COK_KELIME = 14
|
||||||
|
_KILIT = threading.Lock()
|
||||||
|
|
||||||
|
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
|
||||||
|
# Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir.
|
||||||
|
FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim",
|
||||||
|
"degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim",
|
||||||
|
"cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"}
|
||||||
|
OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme")
|
||||||
|
# "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir.
|
||||||
|
SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana",
|
||||||
|
"modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"}
|
||||||
|
SORU = ("hangi", "hangisi", "kim", "kimdir")
|
||||||
|
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
|
||||||
|
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
|
||||||
|
"bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex",
|
||||||
|
"ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona", "nokta"}
|
||||||
|
# Yaziyla soylenen surum rakamlari ("opus bes bes", "uc nokta sekiz"). "bir" belirtec de olabildigi icin
|
||||||
|
# ("bir model sec") yalniz baska bir rakamin ya da "nokta"nin yaninda rakam sayilir.
|
||||||
|
SAYI = {"sifir": "0", "bir": "1", "iki": "2", "uc": "3", "dort": "4", "bes": "5", "alti": "6", "yedi": "7",
|
||||||
|
"sekiz": "8", "dokuz": "9"}
|
||||||
|
# Komutun ardindan gelen ve is tasimayan ek ("... yap ve devam et"): komutu dusurmez, atilir.
|
||||||
|
DEVAM_EKI = {"ve", "sonra", "devam", "et", "edelim", "edin", "lutfen"}
|
||||||
|
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
|
||||||
|
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
|
||||||
|
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
|
||||||
|
"kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex",
|
||||||
|
"dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku",
|
||||||
|
"sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking",
|
||||||
|
"ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"}
|
||||||
|
|
||||||
|
|
||||||
|
# Listede kisa yazilan saglayici -> konusurken soylenen ad
|
||||||
|
SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")}
|
||||||
|
|
||||||
|
|
||||||
|
def duz(metin):
|
||||||
|
return str(metin or "").lower().translate(_TR)
|
||||||
|
|
||||||
|
|
||||||
|
def parcala(metin):
|
||||||
|
"""Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low."""
|
||||||
|
return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))]
|
||||||
|
|
||||||
|
|
||||||
|
def son_sira(girdi):
|
||||||
|
"""Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar
|
||||||
|
kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1."""
|
||||||
|
if not isinstance(girdi, list):
|
||||||
|
return -1
|
||||||
|
for i in range(len(girdi) - 1, -1, -1):
|
||||||
|
oge = girdi[i]
|
||||||
|
if isinstance(oge, dict) and oge.get("type", "message") == "message":
|
||||||
|
if oge.get("role") == "user":
|
||||||
|
return i
|
||||||
|
if oge.get("role") in ("developer", "system"):
|
||||||
|
continue
|
||||||
|
return -1
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def son_kullanici_metni(req):
|
||||||
|
"""(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False).
|
||||||
|
Sesli aktarimda (<realtime_delegation>) yalnizca <input> icindeki soz alinir."""
|
||||||
|
girdi = req.get("input")
|
||||||
|
sira = son_sira(girdi)
|
||||||
|
if sira < 0:
|
||||||
|
return None, -1, False
|
||||||
|
son = girdi[sira]
|
||||||
|
icerik = son.get("content")
|
||||||
|
if isinstance(icerik, str):
|
||||||
|
metin = icerik
|
||||||
|
elif isinstance(icerik, list) and icerik and all(isinstance(p, dict) and p.get("type") in ("input_text", "text")
|
||||||
|
for p in icerik):
|
||||||
|
metin = "".join(str(p.get("text") or "") for p in icerik)
|
||||||
|
else:
|
||||||
|
return None, -1, False
|
||||||
|
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
|
||||||
|
if m:
|
||||||
|
return m.group(1).strip(), sira, True
|
||||||
|
if "<" in metin and ">" in metin:
|
||||||
|
return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz
|
||||||
|
return metin.strip(), sira, False
|
||||||
|
|
||||||
|
|
||||||
|
def _sayilari_cevir(kelimeler):
|
||||||
|
cikti = []
|
||||||
|
for i, k in enumerate(kelimeler):
|
||||||
|
if k in SAYI:
|
||||||
|
komsu = [kelimeler[j] for j in (i - 1, i + 1) if 0 <= j < len(kelimeler)]
|
||||||
|
if k != "bir" or any(x == "nokta" or x.isdigit() or (x in SAYI and x != "bir") for x in komsu):
|
||||||
|
cikti.append(SAYI[k])
|
||||||
|
continue
|
||||||
|
cikti.append(k)
|
||||||
|
return cikti
|
||||||
|
|
||||||
|
|
||||||
|
DOLGU = {"evet", "tamam", "hadi", "peki", "tabii", "hayir", "yani", "sey", "ee", "eee", "hmm", "olur", "simdi"}
|
||||||
|
|
||||||
|
|
||||||
|
def _harfleri_birlestir(kelimeler):
|
||||||
|
"""Harf harf soylenen ad tek sozcuk olur: "s e e d iki nokta bir turbo" -> "seed iki nokta bir turbo"."""
|
||||||
|
cikti, harfler = [], []
|
||||||
|
for k in kelimeler + [""]:
|
||||||
|
if len(k) == 1 and k.isalpha():
|
||||||
|
harfler.append(k)
|
||||||
|
continue
|
||||||
|
if len(harfler) >= 3:
|
||||||
|
cikti.append("".join(harfler))
|
||||||
|
else:
|
||||||
|
cikti.extend(harfler)
|
||||||
|
harfler = []
|
||||||
|
if k:
|
||||||
|
cikti.append(k)
|
||||||
|
# Ad hem heceleyerek hem duz soylendiyse ("seed s e e d ...") tekrar atilir.
|
||||||
|
return [k for i, k in enumerate(cikti) if i == 0 or k != cikti[i - 1]]
|
||||||
|
|
||||||
|
|
||||||
|
def komut_coz(metin):
|
||||||
|
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
|
||||||
|
kelimeler = _harfleri_birlestir(re.findall(r"[a-z0-9]+", duz(metin)))
|
||||||
|
if not kelimeler or len(kelimeler) > EN_COK_KELIME:
|
||||||
|
return None
|
||||||
|
if not any(k.startswith("model") for k in kelimeler):
|
||||||
|
# "modeli" denmeden soylenen komut ("Seed iki nokta bir turbo yap", "gemini flash low'a gec"): yalniz cumle
|
||||||
|
# bir komut fiiliyle bitiyorsa ve ad bir modele TAM uyuyorsa komut sayilir (kesin=False: cagiran, ad
|
||||||
|
# bulunamazsa mesaji komut saymaz, modele birakir).
|
||||||
|
if any(k.startswith(OLUMSUZ) for k in kelimeler) or kelimeler[-1] not in FIILLER or len(kelimeler) > 9:
|
||||||
|
return None
|
||||||
|
kelimeler = _sayilari_cevir(kelimeler)
|
||||||
|
ad = [TAKMA.get(k, k) for k in kelimeler[:-1] if k not in ATLA and k not in DOLGU]
|
||||||
|
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
|
||||||
|
return {"tur": "gec", "ad": ad, "kesin": False} if len(ad) >= 2 else None
|
||||||
|
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
|
||||||
|
return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
|
||||||
|
if any(k.startswith(OLUMSUZ) for k in kelimeler):
|
||||||
|
return None
|
||||||
|
fiil = [k for k in kelimeler if k in FIILLER]
|
||||||
|
if fiil:
|
||||||
|
son_fiil = max(i for i, k in enumerate(kelimeler) if k in FIILLER)
|
||||||
|
if all(k in DEVAM_EKI for k in kelimeler[son_fiil + 1:]):
|
||||||
|
kelimeler = kelimeler[:son_fiil + 1]
|
||||||
|
kelimeler = _sayilari_cevir(kelimeler)
|
||||||
|
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil and k not in DOLGU]
|
||||||
|
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
|
||||||
|
# Ad soylenmemis ("modeli değiştir", "başka modele geç"): degisiklik yok, nasil soylenecegi anlatilir.
|
||||||
|
return {"tur": "gec", "ad": ad, "kesin": True} if ad else {"tur": "nasil"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _uyar(sorgu, parca):
|
||||||
|
"""Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar."""
|
||||||
|
if sorgu == parca:
|
||||||
|
return True
|
||||||
|
return len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit()
|
||||||
|
|
||||||
|
|
||||||
|
def eslestir(ad, modeller):
|
||||||
|
"""ad: sorgu parcalari; modeller: [(slug, gorunen ad)].
|
||||||
|
Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor)."""
|
||||||
|
if not ad:
|
||||||
|
return None
|
||||||
|
puanli = []
|
||||||
|
for slug, gorunen in modeller:
|
||||||
|
parcalar = parcala(gorunen) or parcala(slug)
|
||||||
|
kume = set(parcalar) | set(parcala(slug))
|
||||||
|
for kisa, uzunlar in SAGLAYICI.items():
|
||||||
|
if kisa in kume:
|
||||||
|
kume.update(uzunlar)
|
||||||
|
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
|
||||||
|
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
|
||||||
|
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
|
||||||
|
kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
|
||||||
|
hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
|
||||||
|
puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
|
||||||
|
if not puanli:
|
||||||
|
return None
|
||||||
|
en = max(p[0] for p in puanli)
|
||||||
|
harfler = [s for s in ad if not s.isdigit()]
|
||||||
|
en_harf = max(p[4] for p in puanli)
|
||||||
|
if en_harf < len(harfler):
|
||||||
|
return None # adin bir sozcugu hicbir modelde yok: komut degil
|
||||||
|
iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3]))
|
||||||
|
if not iyi:
|
||||||
|
return None
|
||||||
|
if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme
|
||||||
|
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||||||
|
if len(iyi) == 1:
|
||||||
|
return {"slug": iyi[0][2], "ad": iyi[0][3]}
|
||||||
|
tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model
|
||||||
|
if tam:
|
||||||
|
# Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan,
|
||||||
|
# sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur.
|
||||||
|
def oncelik(p):
|
||||||
|
return (p[2].startswith("ide/"), -p[5], "/" not in p[3])
|
||||||
|
tam.sort(key=oncelik)
|
||||||
|
if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]):
|
||||||
|
return {"slug": tam[0][2], "ad": tam[0][3]}
|
||||||
|
return {"adaylar": [p[3] for p in tam[:4]]}
|
||||||
|
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||||||
|
|
||||||
|
|
||||||
|
_ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning",
|
||||||
|
"local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"}
|
||||||
|
|
||||||
|
|
||||||
|
def tur_ortasi(req, sira):
|
||||||
|
"""Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir:
|
||||||
|
once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir."""
|
||||||
|
girdi = req.get("input") or []
|
||||||
|
for oge in reversed(girdi[:max(sira, 0)]):
|
||||||
|
if not isinstance(oge, dict):
|
||||||
|
continue
|
||||||
|
tur = oge.get("type", "message")
|
||||||
|
if tur in _ARAC:
|
||||||
|
return True
|
||||||
|
if tur == "message":
|
||||||
|
if oge.get("role") == "assistant":
|
||||||
|
return oge.get("phase") == "commentary"
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def metni_degistir(req, sira, yeni):
|
||||||
|
"""sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca <input> icini)."""
|
||||||
|
oge = req["input"][sira]
|
||||||
|
icerik = oge.get("content")
|
||||||
|
eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik)
|
||||||
|
if "<realtime_delegation>" in eski:
|
||||||
|
metin = re.sub(r"(<input>).*?(</input>)", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S)
|
||||||
|
else:
|
||||||
|
metin = yeni
|
||||||
|
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
|
||||||
|
|
||||||
|
|
||||||
|
def gecmisi_temizle(req, modeller, etkin=None):
|
||||||
|
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
|
||||||
|
model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026).
|
||||||
|
etkin: sohbetin su an gercekten kullandigi model. Verilirse, en son kullanici mesajindaki komut (ardindan
|
||||||
|
arac ciktisi ya da sikistirma tetigi geldigi icin "gecmis" gorunen) yalniz bu modele gecirdiyse nota cevrilir;
|
||||||
|
uygulanmamis komut ("baglanti kapali" denen gibi) "degistirildi" diye anlatilmaz.
|
||||||
|
Doner: degistirilen mesaj sayisi."""
|
||||||
|
girdi = req.get("input")
|
||||||
|
if not isinstance(girdi, list):
|
||||||
|
return 0
|
||||||
|
son = son_sira(girdi)
|
||||||
|
son_kullanici = max((i for i, oge in enumerate(girdi) if isinstance(oge, dict)
|
||||||
|
and oge.get("type", "message") == "message" and oge.get("role") == "user"), default=-1)
|
||||||
|
n = 0
|
||||||
|
for i, oge in enumerate(girdi):
|
||||||
|
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||||||
|
continue
|
||||||
|
metin, _, _ = son_kullanici_metni({"input": [oge]})
|
||||||
|
if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"):
|
||||||
|
continue
|
||||||
|
komut = komut_coz(metin)
|
||||||
|
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
|
||||||
|
if sonuc and "slug" in sonuc:
|
||||||
|
if etkin is not None and i == son_kullanici and sonuc["slug"] != etkin:
|
||||||
|
continue
|
||||||
|
metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; "
|
||||||
|
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
BILGI_ISARETI = "[Model değiştirme]"
|
||||||
|
BILGI = (BILGI_ISARETI + " Bu sohbetin modeli söylenerek değiştirilebilir: kullanıcı \"modeli <ad> yap\" dediğinde "
|
||||||
|
"(örnek: \"modeli gemini flash low yap\", \"modeli opus beş beş yap\") değişikliği aradaki köprü yapar; senin bir "
|
||||||
|
"araç çağırman ya da ayar değiştirmen gerekmez. Kullanıcı modeli değiştirip değiştiremeyeceğini sorarsa "
|
||||||
|
"\"değiştiremem\" deme; bu cümleyi söylemesinin yeterli olduğunu kısaca anlat. Hangi modelde olduğunu öğrenmek "
|
||||||
|
"için \"seçili model hangisi\" demesi yeter.")
|
||||||
|
NASIL = ("Hangi modele geçeyim? Adını söylemen yeterli, örneğin: \"modeli gemini flash low yap\" ya da "
|
||||||
|
"\"modeli opus beş beş yap\". Şu anki modeli öğrenmek için \"seçili model hangisi\" de.")
|
||||||
|
|
||||||
|
|
||||||
|
def bilgi_ekle(req):
|
||||||
|
"""Modele, sohbetin modelinin soylenerek degistirilebildigini bildirir (olcum 11 Ekim 2026: Opus "bu sohbetin
|
||||||
|
modelini buradan değiştiremem" dedi). Istegin basina tek developer mesaji; zaten varsa eklenmez. True: eklendi."""
|
||||||
|
girdi = req.get("input")
|
||||||
|
if not isinstance(girdi, list) or not girdi:
|
||||||
|
return False
|
||||||
|
for oge in girdi[:4]:
|
||||||
|
icerik = oge.get("content") if isinstance(oge, dict) else None
|
||||||
|
yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
|
||||||
|
if BILGI_ISARETI in yazi:
|
||||||
|
return False
|
||||||
|
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": BILGI}]})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def devir_notu(yeni_ad, eski_ad):
|
||||||
|
return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren "
|
||||||
|
f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
|
||||||
|
f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]")
|
||||||
|
|
||||||
|
|
||||||
|
class Yonlendirme:
|
||||||
|
"""sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir."""
|
||||||
|
|
||||||
|
def __init__(self, home):
|
||||||
|
self.yol = os.path.join(home, DOSYA) if home else None
|
||||||
|
self.veri = {}
|
||||||
|
if self.yol:
|
||||||
|
try:
|
||||||
|
with open(self.yol, encoding="utf-8") as f:
|
||||||
|
okunan = json.load(f)
|
||||||
|
if isinstance(okunan, dict):
|
||||||
|
self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")}
|
||||||
|
except (OSError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _yaz(self):
|
||||||
|
if not self.yol:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
gecici = self.yol + ".tmp"
|
||||||
|
with open(gecici, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.veri, f, ensure_ascii=False)
|
||||||
|
os.replace(gecici, self.yol)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def al(self, sohbet, gelen, simdi=None):
|
||||||
|
"""Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni
|
||||||
|
model secti) ya da kayit eskidiyse yonlendirme silinir."""
|
||||||
|
simdi = simdi or time.time()
|
||||||
|
with _KILIT:
|
||||||
|
k = self.veri.get(sohbet)
|
||||||
|
if not k:
|
||||||
|
return None
|
||||||
|
if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN:
|
||||||
|
del self.veri[sohbet]
|
||||||
|
self._yaz()
|
||||||
|
return None
|
||||||
|
if simdi - float(k.get("zaman") or 0) > YENILE_SN:
|
||||||
|
# Kullanilan yonlendirme eskimez: omur son kullanimdan sayilir (gunde en cok bir yazma).
|
||||||
|
k["zaman"] = simdi
|
||||||
|
self._yaz()
|
||||||
|
return k["hedef"]
|
||||||
|
|
||||||
|
def koy(self, sohbet, gelen, hedef, simdi=None):
|
||||||
|
simdi = simdi or time.time()
|
||||||
|
with _KILIT:
|
||||||
|
if hedef == gelen:
|
||||||
|
self.veri.pop(sohbet, None)
|
||||||
|
else:
|
||||||
|
self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi}
|
||||||
|
if len(self.veri) > EN_COK_KAYIT:
|
||||||
|
for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]:
|
||||||
|
del self.veri[eski]
|
||||||
|
self._yaz()
|
||||||
@@ -45,6 +45,22 @@ OKUNAMADI_NOTU = ("(Kopru notu: Bu noktadan onceki konusma, onceki modelin sifre
|
|||||||
"aktarilamadi. Onceki kararlari bilmiyorsan uydurma; kullaniciya sor.)")
|
"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.
|
||||||
|
Yalniz bozuk/bos sayilacak kadar kisa ozeti yakalar; ozetin yeterliligini boyuttan cikarmaz.
|
||||||
|
Olcum (10 Ekim 2026, 646.955 baytlik gunluk + bes olgu): guclu modelin dogru ozeti 1500-1700
|
||||||
|
karakterdi; boyuta oranli yuksek esik (2587) bu dogru ozeti reddedip sohbeti kilitledi.
|
||||||
|
Hafiza kaybinin asil onlemi ozeti guclu modele yazdirmaktir (bridge.py _ozet_openai)."""
|
||||||
|
ayar = ayar or {}
|
||||||
|
tavan = int(ayar.get("en_az_tavan", 300))
|
||||||
|
oran = max(1, int(ayar.get("en_az_oran", 2000))) # her 2000 bayt girdi icin 1 karakter
|
||||||
|
return max(0, min(tavan, int(girdi_bayt) // oran))
|
||||||
|
|
||||||
|
|
||||||
def tetik_mi(req):
|
def tetik_mi(req):
|
||||||
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
|
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
|
||||||
ogeler = req.get("input")
|
ogeler = req.get("input")
|
||||||
@@ -129,9 +145,11 @@ class OzetAkisi:
|
|||||||
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
|
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
|
||||||
ic: once calisan cevirici (SseRewriter). Metin gelmezse compaction ogesi uretmez (Codex hatayi gosterir)."""
|
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.ic = ic
|
||||||
self.tohum = tohum
|
self.tohum = tohum
|
||||||
|
self.en_az = en_az # bundan kisa ozet kabul edilmez (bkz. en_az_karakter)
|
||||||
|
self.kisa = False
|
||||||
self.buf = b""
|
self.buf = b""
|
||||||
self.parcalar = {} # item_id -> delta metni
|
self.parcalar = {} # item_id -> delta metni
|
||||||
self.bitenler = [] # tamamlanan mesaj metinleri
|
self.bitenler = [] # tamamlanan mesaj metinleri
|
||||||
@@ -171,6 +189,13 @@ class OzetAkisi:
|
|||||||
self.bitti = True
|
self.bitti = True
|
||||||
if not self.metin:
|
if not self.metin:
|
||||||
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
|
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)
|
oge = ozet_ogesi(self.metin, self.tohum)
|
||||||
yanit = dict(d.get("response") or {})
|
yanit = dict(d.get("response") or {})
|
||||||
yanit["output"] = [oge]
|
yanit["output"] = [oge]
|
||||||
@@ -180,6 +205,14 @@ class OzetAkisi:
|
|||||||
return _olay(d)
|
return _olay(d)
|
||||||
return b""
|
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):
|
def feed(self, chunk):
|
||||||
if self.ic:
|
if self.ic:
|
||||||
chunk = self.ic.feed(chunk)
|
chunk = self.ic.feed(chunk)
|
||||||
@@ -233,10 +266,11 @@ def coz_istegi(oge, model, talimat="Sen bir konusma ozeti aktaricisisin."):
|
|||||||
|
|
||||||
|
|
||||||
def sse_metni(govde):
|
def sse_metni(govde):
|
||||||
"""SSE govdesinden (bayt) asistan metnini toplar."""
|
"""SSE govdesinden (bayt) asistan metnini toplar. Yanit tamamlanmadiysa (akis yarida kesildi) None doner:
|
||||||
|
yarim metin tam ozet diye kalici onbellege yazilmasin."""
|
||||||
a = OzetAkisi()
|
a = OzetAkisi()
|
||||||
a.feed(govde if govde.endswith(b"\n\n") else govde + b"\n\n")
|
a.feed(govde if govde.endswith(b"\n\n") else govde + b"\n\n")
|
||||||
return a.metin if a.bitti else a._topla()
|
return a.metin if a.bitti else None
|
||||||
|
|
||||||
|
|
||||||
# ---- Dongu freni ----
|
# ---- Dongu freni ----
|
||||||
@@ -248,22 +282,35 @@ DONGU_MESAJI = ("AstraBridge: Bu sohbetin baglami, ozetlendikten sonra da secili
|
|||||||
|
|
||||||
|
|
||||||
class DonguFreni:
|
class DonguFreni:
|
||||||
"""Ayni sohbet icin kisa surede cok sayida ozet istegini sayar."""
|
"""Ozet dongusunu durdurur, mesru sik sikistirmayi durdurmaz (10 Ekim 2026: kucuk esikli sohbette
|
||||||
|
10 dakikadaki 4. ozet reddedilip tur dusuyordu). Iki ayri sinir:
|
||||||
|
- art_arda: arada normal (ozet olmayan) istek gecmeden ust uste gelen ozet sayisi; gercek dongu budur.
|
||||||
|
- sinir: pencere_sn icindeki toplam ozet sayisi; her ozetin arasina normal istek giren yavas donguyu keser."""
|
||||||
|
|
||||||
def __init__(self, sinir=3, pencere_sn=600):
|
def __init__(self, sinir=8, pencere_sn=600, art_arda=3):
|
||||||
self.sinir = sinir
|
self.sinir = sinir
|
||||||
self.pencere = pencere_sn
|
self.pencere = pencere_sn
|
||||||
|
self.art_arda = art_arda
|
||||||
self.kayit = {}
|
self.kayit = {}
|
||||||
|
self.ust_uste = {}
|
||||||
|
|
||||||
|
def normal(self, anahtar):
|
||||||
|
"""Bu sohbette ozet olmayan bir istek gecti: art arda sayaci sifirlanir."""
|
||||||
|
self.ust_uste.pop(anahtar, None)
|
||||||
|
|
||||||
def izin(self, anahtar, simdi):
|
def izin(self, anahtar, simdi):
|
||||||
"""Bu ozet istegine izin var mi? Varsa kaydeder."""
|
"""Bu ozet istegine izin var mi? Varsa kaydeder."""
|
||||||
zamanlar = [t for t in self.kayit.get(anahtar, []) if simdi - t < self.pencere]
|
zamanlar = [t for t in self.kayit.get(anahtar, []) if simdi - t < self.pencere]
|
||||||
if len(zamanlar) >= self.sinir:
|
if not zamanlar:
|
||||||
|
self.ust_uste.pop(anahtar, None) # pencere suresi doldu: eski art arda sayaci sohbeti kilitlemesin
|
||||||
|
if len(zamanlar) >= self.sinir or self.ust_uste.get(anahtar, 0) >= self.art_arda:
|
||||||
self.kayit[anahtar] = zamanlar
|
self.kayit[anahtar] = zamanlar
|
||||||
return False
|
return False
|
||||||
zamanlar.append(simdi)
|
zamanlar.append(simdi)
|
||||||
self.kayit[anahtar] = zamanlar
|
self.kayit[anahtar] = zamanlar
|
||||||
|
self.ust_uste[anahtar] = self.ust_uste.get(anahtar, 0) + 1
|
||||||
if len(self.kayit) > 500: # bellek sismesin
|
if len(self.kayit) > 500: # bellek sismesin
|
||||||
for k in sorted(self.kayit, key=lambda k: self.kayit[k][-1])[:250]:
|
for k in sorted(self.kayit, key=lambda k: (self.kayit[k] or [0])[-1])[:250]:
|
||||||
self.kayit.pop(k, None)
|
self.kayit.pop(k, None)
|
||||||
|
self.ust_uste.pop(k, None)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
"""Eklenen modellerin gercek baglam penceresi ve yetenekleri (10 Ekim 2026).
|
||||||
|
|
||||||
|
Neden: gecit model basina pencere vermiyor; kopru her modele abonelik kalibinin penceresini (272000)
|
||||||
|
kopyaliyordu. Codex, kucuk pencereli modele geciste sohbeti kendisi sikistirir, ama bunu ilan edilen
|
||||||
|
pencereye gore yapar (core/src/session/turn.rs maybe_run_previous_model_inline_compact). Pencere dogru
|
||||||
|
ilan edilince o yol kendiliginden calisir; yanlis ilan edilince istek hedefe sigmaz.
|
||||||
|
|
||||||
|
Kaynak sirasi (kucukten guvenilire degil, guvenilirden yedege):
|
||||||
|
1. Hedefin kendi hatasindan ogrenilen sinir (ogren / ogrenilen): en guvenilir, diske yazilir.
|
||||||
|
2. NanoGPT'nin acik model listesi (nano/... modelleri icin istegi tasiyan saglayicinin kendi degeri).
|
||||||
|
3. Saglayici kisaltmasina gore yerel tablo (cc, cx, ag).
|
||||||
|
4. Bilinmeyen: VARSAYILAN (temkinli) ve bir kez gunluk satiri.
|
||||||
|
Combo: yapraklarinin en kucugu (yedege dusulurse de sigsin).
|
||||||
|
Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asmamali.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
TAVAN = 272_000 # abonelik modellerinin penceresi; ozetleyen model bundan fazlasini okuyamaz
|
||||||
|
VARSAYILAN = 128_000 # hakkinda hicbir sey bilinmeyen model
|
||||||
|
TABAN = 8_000 # saglayici verisi bozuksa (0, eksi) kullanilmaz
|
||||||
|
NANO_URL = "https://nano-gpt.com/api/v1/models?detailed=true"
|
||||||
|
NANO_OMUR_SN = 24 * 3600
|
||||||
|
NANO_DOSYA = "pencere-nano.json"
|
||||||
|
OGRENILEN_DOSYA = "pencere-ogrenilen.json"
|
||||||
|
ARACSIZ_DOSYA = "aracsiz-modeller.json"
|
||||||
|
# Saglayici kisaltmasi -> (pencere, gorsel girdi, arac cagrisi). cc = Claude Code aboneligi, cx = Codex
|
||||||
|
# aboneligi, ag = Antigravity. Degerler abonelik yollarinin temkinli sinirlaridir.
|
||||||
|
_YEREL = (
|
||||||
|
(re.compile(r"^cx/"), (272_000, True, True)),
|
||||||
|
(re.compile(r"^cc/"), (200_000, True, True)),
|
||||||
|
(re.compile(r"^ag/claude"), (200_000, True, True)),
|
||||||
|
(re.compile(r"^ag/gemini"), (1_000_000, True, True)),
|
||||||
|
(re.compile(r"^ag/gpt-oss"), (131_072, False, True)),
|
||||||
|
)
|
||||||
|
# "maximum context length is 131072 tokens", "prompt is too long: 3040251 tokens > 1000000 maximum",
|
||||||
|
# "context window of 32768 tokens", "exceeds the limit of 65536" ...
|
||||||
|
_SINIR = (
|
||||||
|
re.compile(r"maximum context length (?:is|of) (\d{4,8})", re.I),
|
||||||
|
re.compile(r"\d{4,9} tokens? > (\d{4,8}) maximum", re.I),
|
||||||
|
re.compile(r"context (?:window|length|size)(?: limit)? (?:is|of) (\d{4,8})", re.I),
|
||||||
|
re.compile(r"(?:limit|maximum) of (\d{4,8}) tokens", re.I),
|
||||||
|
re.compile(r"max(?:imum)? context tokens\D{0,6}(\d{4,8})", re.I),
|
||||||
|
)
|
||||||
|
_TASMA = re.compile(r"context[_ ](?:length|window)[_ ]exceeded|maximum context length|prompt is too long|"
|
||||||
|
r"context window|too many tokens|input is too long|exceeds? the (?:maximum |model.s )?context|max(?:imum)? context tokens", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def _oku(yol):
|
||||||
|
try:
|
||||||
|
with open(yol, encoding="utf-8") as f:
|
||||||
|
v = json.load(f)
|
||||||
|
return v if isinstance(v, dict) else {}
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _yaz(yol, veri):
|
||||||
|
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # es zamanli iki yazma ayni gecici dosyada carpismasin
|
||||||
|
with open(gecici, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(veri, f, ensure_ascii=False)
|
||||||
|
os.replace(gecici, yol)
|
||||||
|
|
||||||
|
|
||||||
|
def nano_sadelestir(ham):
|
||||||
|
"""NanoGPT yaniti -> {model kimligi (kucuk harf): {"pencere", "gorsel", "arac"}}."""
|
||||||
|
cikti = {}
|
||||||
|
for m in (ham or {}).get("data") or []:
|
||||||
|
kimlik = str(m.get("id") or "").lower()
|
||||||
|
pencere = m.get("context_length")
|
||||||
|
if not kimlik or not isinstance(pencere, int) or pencere < TABAN:
|
||||||
|
continue
|
||||||
|
yetenek = m.get("capabilities") or {}
|
||||||
|
cikti[kimlik] = {"pencere": pencere, "gorsel": bool(yetenek.get("vision")),
|
||||||
|
"arac": bool(yetenek.get("tool_calling"))}
|
||||||
|
return cikti
|
||||||
|
|
||||||
|
|
||||||
|
def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir=None, ag=True):
|
||||||
|
"""Onbellek tazeyse onu, degilse agdan yenisini dondurur. Ag hatasinda eski onbellek (yasina bakilmadan)
|
||||||
|
kullanilir; o da yoksa bos sozluk doner ve cagiran yerel tabloya duser. Hicbir durumda istisna atmaz.
|
||||||
|
ag=False: aga cikilmaz, eldeki onbellek doner (koprunun acilisi beklemesin diye)."""
|
||||||
|
simdi = time.time() if simdi is None else simdi
|
||||||
|
yol = os.path.join(home, NANO_DOSYA) if home else ""
|
||||||
|
onbellek = _oku(yol) if yol else {}
|
||||||
|
try:
|
||||||
|
taze = simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
taze = False # bozuk "zaman" alani yenilemeyi engellemez
|
||||||
|
if onbellek.get("modeller") and taze:
|
||||||
|
return onbellek["modeller"]
|
||||||
|
if not ag:
|
||||||
|
return onbellek.get("modeller") or {}
|
||||||
|
try:
|
||||||
|
if getir is None:
|
||||||
|
istek = urllib.request.Request(NANO_URL, headers={"User-Agent": ua, "Accept": "application/json"})
|
||||||
|
with urllib.request.urlopen(istek, timeout=timeout) as resp:
|
||||||
|
ham = json.load(resp)
|
||||||
|
else:
|
||||||
|
ham = getir()
|
||||||
|
modeller = nano_sadelestir(ham)
|
||||||
|
if modeller:
|
||||||
|
if yol:
|
||||||
|
try:
|
||||||
|
_yaz(yol, {"zaman": simdi, "modeller": modeller})
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return modeller
|
||||||
|
except Exception: # noqa: BLE001 - ag, JSON, sertifika: hepsi "yerel tabloya dus" demek
|
||||||
|
pass
|
||||||
|
return onbellek.get("modeller") or {}
|
||||||
|
|
||||||
|
|
||||||
|
def ogrenilen(home):
|
||||||
|
return {k: int(v) for k, v in _oku(os.path.join(home, OGRENILEN_DOSYA)).items()
|
||||||
|
if isinstance(v, (int, float)) and v >= TABAN} if home else {}
|
||||||
|
|
||||||
|
|
||||||
|
def aracsizlar(home):
|
||||||
|
"""Hedefin "arac cagrisi desteklenmiyor" dedigi modeller (kimlik kumesi)."""
|
||||||
|
return {k for k, v in _oku(os.path.join(home, ARACSIZ_DOSYA)).items() if v} if home else set()
|
||||||
|
|
||||||
|
|
||||||
|
def aracsiz_mi(metin):
|
||||||
|
return bool(re.search(r"does not support tool|tool_choice_unsupported|tools? (?:are|is) not supported|"
|
||||||
|
r"function calling is not supported", metin or "", re.I))
|
||||||
|
|
||||||
|
|
||||||
|
def ogren_aracsiz(home, kimlik):
|
||||||
|
"""True: kayit yeni eklendi."""
|
||||||
|
if not home or not kimlik:
|
||||||
|
return False
|
||||||
|
yol = os.path.join(home, ARACSIZ_DOSYA)
|
||||||
|
veri = _oku(yol)
|
||||||
|
if veri.get(kimlik):
|
||||||
|
return False
|
||||||
|
veri[kimlik] = True
|
||||||
|
try:
|
||||||
|
_yaz(yol, veri)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def kapali_hata_mi(metin):
|
||||||
|
"""Gecit asil nedeni gizleyip yalniz "upstream 400" derse (olcum 11 Ekim 2026)."""
|
||||||
|
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
|
||||||
|
|
||||||
|
|
||||||
|
_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
|
||||||
|
|
||||||
|
|
||||||
|
def hiz_siniri_mi(metin):
|
||||||
|
"""Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
|
||||||
|
return bool(_HIZ_SINIRI.search(metin or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def tasma_mi(metin):
|
||||||
|
return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
|
||||||
|
|
||||||
|
|
||||||
|
def sinir_oku(metin):
|
||||||
|
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
|
||||||
|
if hiz_siniri_mi(metin):
|
||||||
|
return None
|
||||||
|
for kalip in _SINIR:
|
||||||
|
m = kalip.search(metin or "")
|
||||||
|
if m and int(m.group(1)) >= TABAN:
|
||||||
|
return int(m.group(1))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ogren(home, kimlik, sinir):
|
||||||
|
"""Hedefin bildirdigi siniri kaydeder (yalniz kucultur). True: kayit degisti."""
|
||||||
|
if not home or not kimlik or not sinir or sinir < TABAN:
|
||||||
|
return False
|
||||||
|
yol = os.path.join(home, OGRENILEN_DOSYA)
|
||||||
|
veri = _oku(yol)
|
||||||
|
if isinstance(veri.get(kimlik), (int, float)) and veri[kimlik] <= sinir:
|
||||||
|
return False
|
||||||
|
veri[kimlik] = int(sinir)
|
||||||
|
try:
|
||||||
|
_yaz(yol, veri)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def yol_bilgisi(yol, nano):
|
||||||
|
""""nano/z-ai/glm-5.3" ya da "cc/claude-opus-5-5" -> {"pencere","gorsel","arac","kaynak"} ya da None."""
|
||||||
|
yol = (yol or "").strip()
|
||||||
|
if yol.startswith("9router/"):
|
||||||
|
yol = yol[len("9router/"):]
|
||||||
|
if yol.startswith("nano/"):
|
||||||
|
n = (nano or {}).get(yol[len("nano/"):].lower())
|
||||||
|
if n:
|
||||||
|
return {**n, "kaynak": "nanogpt"}
|
||||||
|
return None
|
||||||
|
for kalip, (pencere, gorsel, arac) in _YEREL:
|
||||||
|
if kalip.match(yol):
|
||||||
|
return {"pencere": pencere, "gorsel": gorsel, "arac": arac, "kaynak": "yerel"}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _yapraklar(dugum, derinlik=0):
|
||||||
|
if not isinstance(dugum, dict) or derinlik > 6:
|
||||||
|
return []
|
||||||
|
if dugum.get("kind") != "combo":
|
||||||
|
return [dugum.get("model")] if dugum.get("model") else []
|
||||||
|
cikti = []
|
||||||
|
for uye in dugum.get("members") or []:
|
||||||
|
cikti.extend(_yapraklar(uye, derinlik + 1))
|
||||||
|
return cikti
|
||||||
|
|
||||||
|
|
||||||
|
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
|
||||||
|
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
|
||||||
|
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
|
||||||
|
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
|
||||||
|
bilinmeyen = {"pencere": VARSAYILAN, "gorsel": False, "arac": True, "kaynak": "varsayilan"}
|
||||||
|
if tur == "combo":
|
||||||
|
yapraklar = _yapraklar((kayit or {}).get("topology"))
|
||||||
|
bilgiler = [yol_bilgisi(y, nano) or dict(bilinmeyen) for y in yapraklar]
|
||||||
|
if not bilgiler:
|
||||||
|
sonuc = dict(bilinmeyen)
|
||||||
|
else:
|
||||||
|
sonuc = {"pencere": min(b["pencere"] for b in bilgiler),
|
||||||
|
# Bir takim uyesi resmi goremese de digerleri gorur; sirali yedekte ilk uye belirler.
|
||||||
|
"gorsel": bool(bilgiler[0]["gorsel"]), "arac": bool(bilgiler[0]["arac"]),
|
||||||
|
"kaynak": "uyeler:" + ",".join(sorted({b["kaynak"] for b in bilgiler}))}
|
||||||
|
else:
|
||||||
|
if (yol or "").startswith("no-think/"):
|
||||||
|
# Dusunmesiz kopya: asil saglayici kimlikten okunur ("no-think-9router-cc-claude-..." -> cc).
|
||||||
|
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
|
||||||
|
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
|
||||||
|
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
|
||||||
|
if kimlik in aracsiz:
|
||||||
|
sonuc = {**sonuc, "arac": False}
|
||||||
|
sinir = (ogr or {}).get(kimlik)
|
||||||
|
if sinir and sinir < sonuc["pencere"]:
|
||||||
|
sonuc = {**sonuc, "pencere": int(sinir), "kaynak": "ogrenilen"}
|
||||||
|
sonuc["pencere"] = max(TABAN, min(TAVAN, int(sonuc["pencere"])))
|
||||||
|
return sonuc
|
||||||
|
|
||||||
|
|
||||||
|
def claude_var(tur, yol, kayit=None):
|
||||||
|
"""Istek Claude'a gidebilir mi? Tek modelde yolundan, comboda yapraklarindan okunur (Opus-5 gibi combolarin
|
||||||
|
adinda "claude" gecmez; token tahmini yine Claude oraniyla yapilmalidir)."""
|
||||||
|
yollar = _yapraklar((kayit or {}).get("topology")) if tur == "combo" else [yol]
|
||||||
|
for y in yollar:
|
||||||
|
y = str(y or "").lower()
|
||||||
|
if y.startswith("9router/"):
|
||||||
|
y = y[len("9router/"):]
|
||||||
|
if "claude" in y or y.startswith("cc/"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def bekci_calisir(kaynak, pencere_token):
|
||||||
|
"""Tasma bekcisi bu pencereye guvenebilir mi? Hakkinda hicbir sey bilinmeyen modelde calismaz (tahmine
|
||||||
|
dayanarak gecmis kirpilmaz). Comboda tek bilinmeyen uye, bilinen KUCUK uyenin sinirini gecersiz kilmaz:
|
||||||
|
ilan edilen pencere VARSAYILAN'dan kucukse o deger bilinen bir uyeden gelmistir."""
|
||||||
|
kaynak = kaynak or "varsayilan"
|
||||||
|
if "varsayilan" not in kaynak:
|
||||||
|
return True
|
||||||
|
return kaynak.startswith("uyeler:") and kaynak != "uyeler:varsayilan" and int(pencere_token or 0) < VARSAYILAN
|
||||||
|
|
||||||
|
|
||||||
|
def kisa(pencere):
|
||||||
|
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
|
||||||
|
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Istek oncesi tasma bekcisi ----
|
||||||
|
# Codex sikistirmadan sonra kullanici mesajlarini AYNEN tutar; cok buyuk bir yapistirma (ya da dev arac
|
||||||
|
# ciktisi) kucuk pencereli hedefe yine sigmaz ve hedef bos akisla doner (10 Ekim 2026 olcumu: 390 KB istek,
|
||||||
|
# 64K pencere, Codex 5 kez yeniden deneyip "stream closed" ile dustu). Bekci, hedefe giden KOPYADA en buyuk
|
||||||
|
# eski metin parcalarinin ortasini cikarir; yine sigmazsa istegi gondermeden anlasilir hata doner.
|
||||||
|
BAYT_BOLU_TOKEN = 3.2 # olcum (10 Ekim 2026): 540 KB duz Turkce metin = 171016 token (3,16 bayt/token)
|
||||||
|
DOLULUK = 0.97 # pencerenin bu oranini asan istek kirpilir / gonderilmez
|
||||||
|
KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
|
||||||
|
KIRP_BAS = KIRP_SON = 1500
|
||||||
|
|
||||||
|
|
||||||
|
# Claude ayni metni daha cok tokena boler. Olcum (11 Ekim 2026): 607 KB'lik gercek Codex istegi 3,2 oraniyla
|
||||||
|
# 189776 token tahmin edildi (pencere 200000) ve Claude tarafindan reddedildi; yani gercek oran 3,03'un altinda.
|
||||||
|
CLAUDE_BAYT_BOLU_TOKEN = 2.6
|
||||||
|
|
||||||
|
|
||||||
|
def oran_sec(hedef, claude=False):
|
||||||
|
"""claude=True: hedefin adinda gecmese de (combo) istek Claude'a gidebilir (bkz. claude_var)."""
|
||||||
|
return CLAUDE_BAYT_BOLU_TOKEN if (claude or "claude" in str(hedef or "").lower()) else BAYT_BOLU_TOKEN
|
||||||
|
|
||||||
|
|
||||||
|
def tahmini_token(req, oran=None):
|
||||||
|
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / (oran or BAYT_BOLU_TOKEN))
|
||||||
|
|
||||||
|
|
||||||
|
KOPRU_DEVAM_BASI = "(Önceki cevabın yarıda kaldı" # bridge.py claude_sonu_duzelt'in ekledigi mesajin basi
|
||||||
|
|
||||||
|
|
||||||
|
def _kopru_mesaji(oge):
|
||||||
|
icerik = oge.get("content")
|
||||||
|
if isinstance(icerik, list):
|
||||||
|
icerik = "".join(str(p.get("text") or "") for p in icerik if isinstance(p, dict))
|
||||||
|
return isinstance(icerik, str) and icerik.startswith(KOPRU_DEVAM_BASI)
|
||||||
|
|
||||||
|
|
||||||
|
def _son_arac_ciktisi(req):
|
||||||
|
"""En yeni arac ciktisinin metin sahipleri (kimlik kumesi): model bir sonraki adimi buna gore atar,
|
||||||
|
bu yuzden ancak baska hicbir sey yetmezse kirpilir."""
|
||||||
|
for it in reversed(req.get("input") or []):
|
||||||
|
if isinstance(it, dict) and "output" in it and str(it.get("type") or "").endswith("_output"):
|
||||||
|
deger = it.get("output")
|
||||||
|
return {id(it)} if isinstance(deger, str) else {id(p) for p in deger or [] if isinstance(p, dict)}
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def _metin_parcalari(req):
|
||||||
|
"""Kirpilabilir (sahip, anahtar) ciftleri: arac ciktilari ile eski kullanici/asistan mesajlari.
|
||||||
|
Son kullanici mesajina ve talimat tasiyan developer/system mesajlarina (Codex kurallari, skill
|
||||||
|
yonergeleri, kanca baglami) dokunulmaz."""
|
||||||
|
girdi = req.get("input")
|
||||||
|
if not isinstance(girdi, list):
|
||||||
|
return []
|
||||||
|
# Koprunun sona ekledigi "devam et" mesaji kullanicinin son mesaji degildir; korunan, ondan onceki gercek mesajdir.
|
||||||
|
son_kullanici = max((i for i, it in enumerate(girdi) if isinstance(it, dict) and it.get("role") == "user"
|
||||||
|
and it.get("type", "message") == "message" and not _kopru_mesaji(it)), default=-1)
|
||||||
|
parcalar = []
|
||||||
|
for i, it in enumerate(girdi):
|
||||||
|
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
|
||||||
|
continue
|
||||||
|
for alan in ("content", "output"):
|
||||||
|
deger = it.get(alan)
|
||||||
|
if isinstance(deger, str):
|
||||||
|
parcalar.append((it, alan))
|
||||||
|
elif isinstance(deger, list):
|
||||||
|
for p in deger:
|
||||||
|
if isinstance(p, dict) and isinstance(p.get("text"), str):
|
||||||
|
parcalar.append((p, "text"))
|
||||||
|
return parcalar
|
||||||
|
|
||||||
|
|
||||||
|
def sigdir(req, pencere_token, oran=None):
|
||||||
|
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
|
||||||
|
sinir = int(pencere_token * DOLULUK)
|
||||||
|
tahmin = tahmini_token(req, oran)
|
||||||
|
if not pencere_token or tahmin <= sinir:
|
||||||
|
return 0, True, tahmin
|
||||||
|
kirpilan = 0
|
||||||
|
en_yeni = _son_arac_ciktisi(req)
|
||||||
|
# Buyukten kucuge; en yeni arac ciktisi en sona kalir (yalniz digerleri yetmezse kirpilir).
|
||||||
|
parcalar = sorted(_metin_parcalari(req), key=lambda sa: (id(sa[0]) in en_yeni, -len(sa[0][sa[1]])))
|
||||||
|
for sahip, anahtar in parcalar:
|
||||||
|
metin = sahip[anahtar]
|
||||||
|
if len(metin) < KIRP_ESIK:
|
||||||
|
continue
|
||||||
|
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
|
||||||
|
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
|
||||||
|
kirpilan += 1
|
||||||
|
tahmin = tahmini_token(req, oran)
|
||||||
|
if tahmin <= sinir:
|
||||||
|
return kirpilan, True, tahmin
|
||||||
|
return kirpilan, tahmin <= sinir, tahmin
|
||||||
@@ -49,14 +49,18 @@ def _kisalt(metin):
|
|||||||
+ metin[-yarim:])
|
+ metin[-yarim:])
|
||||||
|
|
||||||
|
|
||||||
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
|
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
|
||||||
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir."""
|
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir.
|
||||||
|
gorsel=True (hedef model resim görebiliyor): eski KULLANICI mesajlarındaki resimler de, en yeniden eskiye
|
||||||
|
toplam tut_bayt'ı aşmadıkça korunur (10 Ekim 2026: resim görebilen modele geçince "göremiyorum" diyordu).
|
||||||
|
Araç çıktılarındaki medya her durumda notla değiştirilir."""
|
||||||
inp = req.get('input')
|
inp = req.get('input')
|
||||||
if not isinstance(inp, list):
|
if not isinstance(inp, list):
|
||||||
return 0, 0
|
return 0, 0
|
||||||
son_kullanici = None
|
son_kullanici = None
|
||||||
for i, it in enumerate(inp):
|
for i, it in enumerate(inp):
|
||||||
if isinstance(it, dict) and it.get('type') == 'message' and it.get('role') == 'user':
|
# 'type' alani olmayan mesaj da mesajdir (Responses girdisinde varsayilan tur).
|
||||||
|
if isinstance(it, dict) and it.get('type', 'message') == 'message' and it.get('role') == 'user':
|
||||||
son_kullanici = i
|
son_kullanici = i
|
||||||
koru = set()
|
koru = set()
|
||||||
if son_kullanici is not None:
|
if son_kullanici is not None:
|
||||||
@@ -65,6 +69,20 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
|
|||||||
medya = [p for p in icerik if _medya_mi(p)]
|
medya = [p for p in icerik if _medya_mi(p)]
|
||||||
if medya and sum(_boyut(p) for p in medya) <= tut_bayt:
|
if medya and sum(_boyut(p) for p in medya) <= tut_bayt:
|
||||||
koru.add(son_kullanici)
|
koru.add(son_kullanici)
|
||||||
|
if gorsel:
|
||||||
|
kalan = tut_bayt - sum(_boyut(p) for i in koru for p in inp[i].get('content') or [] if _medya_mi(p))
|
||||||
|
for i in range(len(inp) - 1, -1, -1):
|
||||||
|
it = inp[i]
|
||||||
|
if i in koru or not (isinstance(it, dict) and it.get('type', 'message') == 'message' and it.get('role') == 'user'):
|
||||||
|
continue
|
||||||
|
icerik = it.get('content')
|
||||||
|
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
|
||||||
|
if isinstance(icerik, list) else []
|
||||||
|
diger = [p for p in icerik if _medya_mi(p) and p not in resimler] if isinstance(icerik, list) else []
|
||||||
|
boyut = sum(_boyut(p) for p in resimler)
|
||||||
|
if resimler and not diger and boyut <= kalan:
|
||||||
|
koru.add(i)
|
||||||
|
kalan -= boyut
|
||||||
sayi = kazanc = 0
|
sayi = kazanc = 0
|
||||||
for i, it in enumerate(inp):
|
for i, it in enumerate(inp):
|
||||||
if not isinstance(it, dict) or i in koru:
|
if not isinstance(it, dict) or i in koru:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AccessPrograms(unittest.TestCase):
|
|||||||
"fix_tool_ids": lambda req: 0,
|
"fix_tool_ids": lambda req: 0,
|
||||||
"drop_foreign_items": lambda req: 0,
|
"drop_foreign_items": lambda req: 0,
|
||||||
"SseRewriter": Mock(),
|
"SseRewriter": Mock(),
|
||||||
"log": lambda message: None,
|
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "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,
|
"DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900,
|
||||||
}
|
}
|
||||||
exec(compile(ast.Module(body=[flatten, claude_sonu, responses], type_ignores=[]), "bridge.py", "exec"), self.env)
|
exec(compile(ast.Module(body=[flatten, claude_sonu, responses], type_ignores=[]), "bridge.py", "exec"), self.env)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ComboNotResent(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.upstream = Mock()
|
self.upstream = Mock()
|
||||||
self.env = {
|
self.env = {
|
||||||
"json": json, "re": __import__("re"), "os": __import__("os"),
|
"json": json, "re": __import__("re"), "os": __import__("os"), "http": __import__("http.client"),
|
||||||
"time": types.SimpleNamespace(sleep=lambda s: None),
|
"time": types.SimpleNamespace(sleep=lambda s: None),
|
||||||
"urllib": types.SimpleNamespace(
|
"urllib": types.SimpleNamespace(
|
||||||
request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=self.upstream),
|
request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=self.upstream),
|
||||||
@@ -40,9 +40,9 @@ class ComboNotResent(unittest.TestCase):
|
|||||||
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
|
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0,
|
||||||
"SseRewriter": Mock(), "log": lambda message: None,
|
"SseRewriter": Mock(), "log": lambda message: None,
|
||||||
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
|
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
|
||||||
"NO_RETRY_PATTERNS": [],
|
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "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("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses],
|
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), pick("kopma_mi"), responses],
|
||||||
type_ignores=[]), "bridge.py", "exec"), self.env)
|
type_ignores=[]), "bridge.py", "exec"), self.env)
|
||||||
|
|
||||||
def send(self, model):
|
def send(self, model):
|
||||||
@@ -85,6 +85,16 @@ class ComboNotResent(unittest.TestCase):
|
|||||||
self.assertEqual(self.upstream.call_count, 3)
|
self.assertEqual(self.upstream.call_count, 3)
|
||||||
fake._stream.assert_called_once()
|
fake._stream.assert_called_once()
|
||||||
|
|
||||||
|
def test_combo_closed_by_the_peer_before_headers_is_not_resent(self):
|
||||||
|
import http.client
|
||||||
|
for error in (http.client.RemoteDisconnected("closed"), urllib.error.URLError(ConnectionResetError("reset"))):
|
||||||
|
with self.subTest(error=error):
|
||||||
|
self.upstream.reset_mock()
|
||||||
|
self.upstream.side_effect = error
|
||||||
|
fake = self.send("9r/Fusion-Orta")
|
||||||
|
self.assertEqual(self.upstream.call_count, 1)
|
||||||
|
fake._stream.assert_not_called()
|
||||||
|
|
||||||
def test_single_model_timeout_is_still_retried(self):
|
def test_single_model_timeout_is_still_retried(self):
|
||||||
self.upstream.side_effect = [urllib.error.URLError(TimeoutError("timed out")), Mock(status=200)]
|
self.upstream.side_effect = [urllib.error.URLError(TimeoutError("timed out")), Mock(status=200)]
|
||||||
fake = self.send("9r/cc/claude-opus-5-5")
|
fake = self.send("9r/cc/claude-opus-5-5")
|
||||||
|
|||||||
@@ -0,0 +1,441 @@
|
|||||||
|
"""Audit of 11 Oct 2026, remaining findings: each fix has a test here (handler methods run through a fake handler)."""
|
||||||
|
import ast
|
||||||
|
import copy
|
||||||
|
import gzip
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import ide_catalog
|
||||||
|
import is_hatti
|
||||||
|
import model_gecis
|
||||||
|
import ozet_kopru
|
||||||
|
import pencere
|
||||||
|
import resim_context
|
||||||
|
import yarim
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).with_name("bridge.py").read_text(encoding="utf-8")
|
||||||
|
tree = ast.parse(SOURCE)
|
||||||
|
handler = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "H")
|
||||||
|
|
||||||
|
|
||||||
|
def top(*names):
|
||||||
|
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in names for t in node.targets):
|
||||||
|
nodes.insert(0, node)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def method(name):
|
||||||
|
return next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == name)
|
||||||
|
|
||||||
|
|
||||||
|
def build(nodes, env):
|
||||||
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def user(text):
|
||||||
|
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
|
||||||
|
|
||||||
|
|
||||||
|
def developer(text):
|
||||||
|
return {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": text}]}
|
||||||
|
|
||||||
|
|
||||||
|
MODELS = [("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("gpt-6.1-sol", "GPT-6.1-Sol"),
|
||||||
|
("ide/9router-ag-gemini-3.8-flash-low", "agy/gemini-3.8-flash-low"),
|
||||||
|
("ide/9router-cc-claude-opus-5-5", "cc/opus-5-5")]
|
||||||
|
CALL = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
|
||||||
|
OUT = {"type": "function_call_output", "call_id": "c", "output": "x"}
|
||||||
|
|
||||||
|
|
||||||
|
class SesliKomut(unittest.TestCase):
|
||||||
|
def pick(self, text):
|
||||||
|
command = model_gecis.komut_coz(text)
|
||||||
|
return model_gecis.eslestir(command["ad"], MODELS) if command and command["tur"] == "gec" else None
|
||||||
|
|
||||||
|
def test_numbers_said_as_words_and_a_continue_tail_still_switch(self):
|
||||||
|
for text, slug in [("modeli opus beş beş yap", "ide/9router-cc-claude-opus-5-5"),
|
||||||
|
("modeli gemini üç nokta sekiz flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
|
||||||
|
("modeli gpt altı nokta bir sol yap", "gpt-6.1-sol"),
|
||||||
|
("modeli gpt altı astra yap ve devam et", "gpt-6-astra"),
|
||||||
|
("modeli opus 5 5 yap sonra devam et", "ide/9router-cc-claude-opus-5-5")]:
|
||||||
|
self.assertEqual((self.pick(text) or {}).get("slug"), slug, text)
|
||||||
|
|
||||||
|
def test_a_task_after_the_command_is_not_swallowed(self):
|
||||||
|
for text in ("modeli opus 5 5 yap ve şu dosyayı düzelt", "bir model seç", "modeli iki kere değiştir"):
|
||||||
|
self.assertIsNone(self.pick(text), text)
|
||||||
|
|
||||||
|
def test_last_command_is_noted_only_when_it_really_took_effect(self):
|
||||||
|
opus = "ide/9router-cc-claude-opus-5-5"
|
||||||
|
refused = {"input": [user("modeli opus 5 5 yap"), {"type": "compaction_trigger"}]}
|
||||||
|
self.assertEqual(model_gecis.gecmisi_temizle(refused, MODELS, "gpt-6-sol"), 0) # e.g. "bağlantı kapalı"
|
||||||
|
self.assertEqual(refused["input"][0]["content"][0]["text"], "modeli opus 5 5 yap")
|
||||||
|
applied = {"input": [user("modeli opus 5 5 yap"), CALL, OUT]}
|
||||||
|
self.assertEqual(model_gecis.gecmisi_temizle(applied, MODELS, opus), 1) # mid-turn handover
|
||||||
|
self.assertIn("cc/opus-5-5 olarak değiştirdi", applied["input"][0]["content"][0]["text"])
|
||||||
|
|
||||||
|
def test_a_route_in_use_does_not_expire(self):
|
||||||
|
y = model_gecis.Yonlendirme(None)
|
||||||
|
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
|
||||||
|
day = 24 * 3600
|
||||||
|
for n in range(1, 12): # used every 1.5 days for 16 days
|
||||||
|
self.assertEqual(y.al("chat", "gpt-6-sol", simdi=1000 + n * 1.5 * day), "ide/x", n)
|
||||||
|
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1000 + 16.5 * day + model_gecis.OMUR_SN + 1)) # unused for a week
|
||||||
|
|
||||||
|
|
||||||
|
class Pencere(unittest.TestCase):
|
||||||
|
def test_guard_runs_for_a_combo_whose_small_member_is_known(self):
|
||||||
|
self.assertFalse(pencere.bekci_calisir("varsayilan", 128_000))
|
||||||
|
self.assertFalse(pencere.bekci_calisir("", 128_000))
|
||||||
|
self.assertFalse(pencere.bekci_calisir("uyeler:varsayilan", 128_000))
|
||||||
|
self.assertTrue(pencere.bekci_calisir("uyeler:nanogpt,varsayilan", 8192)) # the 8K member is real
|
||||||
|
self.assertFalse(pencere.bekci_calisir("uyeler:nanogpt,varsayilan", 128_000)) # the minimum is the guess
|
||||||
|
for known in ("yerel", "nanogpt", "ogrenilen", "uyeler:nanogpt,yerel"):
|
||||||
|
self.assertTrue(pencere.bekci_calisir(known, 200_000), known)
|
||||||
|
|
||||||
|
def test_combo_with_a_claude_member_uses_the_claude_ratio(self):
|
||||||
|
combo = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||||||
|
{"kind": "model", "model": "cx/gpt-6-astra"},
|
||||||
|
{"kind": "combo", "strategy": "priority", "members": [{"kind": "model", "model": "cc/claude-opus-5-5"}]}]}}
|
||||||
|
plain = {"topology": {"kind": "combo", "members": [{"kind": "model", "model": "ag/gemini-3.8-flash"}]}}
|
||||||
|
self.assertTrue(pencere.claude_var("combo", "Opus-5", combo))
|
||||||
|
self.assertFalse(pencere.claude_var("combo", "Hizli", plain))
|
||||||
|
self.assertTrue(pencere.claude_var("model", "no-think/claude-opus-5-5"))
|
||||||
|
self.assertFalse(pencere.claude_var("model", "nano/z-ai/glm-5.3"))
|
||||||
|
self.assertEqual(pencere.oran_sec("Opus-5", True), pencere.CLAUDE_BAYT_BOLU_TOKEN)
|
||||||
|
self.assertEqual(pencere.oran_sec("Opus-5"), pencere.BAYT_BOLU_TOKEN)
|
||||||
|
|
||||||
|
def test_a_corrupt_nano_cache_does_not_block_the_refresh(self):
|
||||||
|
home = tempfile.mkdtemp()
|
||||||
|
Path(home, pencere.NANO_DOSYA).write_text(json.dumps({"zaman": "dun", "modeller": {"eski": {"pencere": 9000}}}),
|
||||||
|
encoding="utf-8")
|
||||||
|
fresh = {"data": [{"id": "Yeni/M", "context_length": 65536, "capabilities": {}}]}
|
||||||
|
self.assertIn("yeni/m", pencere.nano_yukle(home, simdi=1000.0, getir=lambda: fresh))
|
||||||
|
|
||||||
|
def test_trimming_keeps_the_real_last_user_message_and_the_newest_tool_output(self):
|
||||||
|
marker = "(Önceki cevabın yarıda kaldı ya da sesli konuşma kapandı; bağlama göre devam et.)"
|
||||||
|
self.assertIn(marker, SOURCE) # the text bridge.py appends
|
||||||
|
self.assertTrue(marker.startswith(pencere.KOPRU_DEVAM_BASI))
|
||||||
|
req = {"input": [{"type": "function_call_output", "call_id": "a", "output": "eski " * 30000},
|
||||||
|
user("TALIMAT " * 5000),
|
||||||
|
{"type": "function_call_output", "call_id": "b", "output": "yeni " * 4000},
|
||||||
|
user(marker)]}
|
||||||
|
trimmed, fits, _ = pencere.sigdir(req, 30_000)
|
||||||
|
self.assertEqual((trimmed, fits), (1, True))
|
||||||
|
self.assertIn("çıkarıldı (köprü)", req["input"][0]["output"])
|
||||||
|
self.assertEqual(req["input"][1]["content"][0]["text"], "TALIMAT " * 5000) # the user's real last message
|
||||||
|
self.assertEqual(req["input"][2]["output"], "yeni " * 4000) # newest tool result untouched
|
||||||
|
# When nothing else is left, the newest tool output is trimmed rather than the request refused.
|
||||||
|
only = {"input": [user("soru"), {"type": "function_call_output", "call_id": "b", "output": "yeni " * 40000}]}
|
||||||
|
self.assertEqual(pencere.sigdir(only, 30_000)[:2], (1, True))
|
||||||
|
|
||||||
|
|
||||||
|
class Ozet(unittest.TestCase):
|
||||||
|
def test_a_cut_stream_is_not_returned_as_a_whole_summary(self):
|
||||||
|
delta = b'data: {"type":"response.output_text.delta","item_id":"m","delta":"yarim oz"}\n\n'
|
||||||
|
self.assertIsNone(ozet_kopru.sse_metni(delta))
|
||||||
|
done = delta + b'data: {"type":"response.completed","response":{}}\n\n'
|
||||||
|
self.assertEqual(ozet_kopru.sse_metni(done), "yarim oz")
|
||||||
|
|
||||||
|
def test_the_in_a_row_brake_lifts_after_the_window(self):
|
||||||
|
f = ozet_kopru.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
|
||||||
|
self.assertEqual([f.izin("s", t) for t in (0, 10, 20, 30)], [True, True, True, False])
|
||||||
|
self.assertFalse(f.izin("s", 500)) # still inside the window
|
||||||
|
self.assertTrue(f.izin("s", 24 * 3600)) # a day later the chat is not locked any more
|
||||||
|
|
||||||
|
def test_cleanup_survives_an_empty_record(self):
|
||||||
|
f = ozet_kopru.DonguFreni()
|
||||||
|
f.kayit = {"k%d" % i: [float(i)] for i in range(500)}
|
||||||
|
f.kayit["bos"] = []
|
||||||
|
self.assertTrue(f.izin("yeni", 1000.0))
|
||||||
|
self.assertLess(len(f.kayit), 300)
|
||||||
|
|
||||||
|
|
||||||
|
class Akis(unittest.TestCase):
|
||||||
|
env = build(top("fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "ID_PREFIX", "ARAC_OGELERI"),
|
||||||
|
{"json": json, "re": __import__("re"), "time": time, "log": lambda m: None, "pencere": pencere})
|
||||||
|
|
||||||
|
def events(self, raw):
|
||||||
|
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
|
||||||
|
|
||||||
|
def test_crlf_separated_events_are_translated(self):
|
||||||
|
flat = {"type": "error", "code": "bad_request", "message": "upstream 402"}
|
||||||
|
raw = b"event: error\r\ndata: " + json.dumps(flat).encode() + b"\r\n\r\n"
|
||||||
|
adapter = self.env["SseRewriter"](set())
|
||||||
|
out = self.events(adapter.feed(raw[:20]) + adapter.feed(raw[20:]) + adapter.flush())
|
||||||
|
self.assertEqual(out[-1]["type"], "response.completed")
|
||||||
|
self.assertIn("kredi ya da kota", out[1]["delta"])
|
||||||
|
watcher = yarim.Izleyici("k", "m")
|
||||||
|
watcher.feed(b'data: {"type":"response.output_text.delta","delta":"merhaba"}\r\n\r\n'
|
||||||
|
b'data: {"type":"response.completed"}\r\n\r\n')
|
||||||
|
self.assertEqual((watcher.metin, watcher.bitti), ("merhaba", True))
|
||||||
|
|
||||||
|
def test_a_non_object_event_body_passes_through(self):
|
||||||
|
adapter = self.env["SseRewriter"](set())
|
||||||
|
self.assertEqual(adapter.feed(b"data: null\n\ndata: [1]\n\n"), b"data: null\n\ndata: [1]\n\n")
|
||||||
|
|
||||||
|
def test_a_waiting_message_is_released_before_the_error_answer(self):
|
||||||
|
message = {"type": "response.output_item.done", "output_index": 0,
|
||||||
|
"item": {"id": "m1", "type": "message", "role": "assistant", "content": []}}
|
||||||
|
adapter = self.env["SseRewriter"](set(), dolu=True)
|
||||||
|
first = adapter.feed(b"data: " + json.dumps(message).encode() + b"\n\n")
|
||||||
|
self.assertEqual(first, b"") # held until what follows is known
|
||||||
|
out = self.events(adapter.feed(b'data: {"type":"error","message":"upstream 400"}\n\n') + adapter.flush())
|
||||||
|
self.assertEqual([e["type"] for e in out], ["response.output_item.done", "response.failed"])
|
||||||
|
self.assertEqual(out[0]["item"]["id"], "m1")
|
||||||
|
self.assertIn("nedenini bildirmedi", out[1]["response"]["error"]["message"]) # no false "did not fit"
|
||||||
|
self.assertIn("olabilir", out[1]["response"]["error"]["message"])
|
||||||
|
|
||||||
|
|
||||||
|
class Resim(unittest.TestCase):
|
||||||
|
def test_a_new_image_in_a_message_without_type_is_kept(self):
|
||||||
|
img = {"type": "input_image", "image_url": "data:image/png;base64," + "A" * 2000}
|
||||||
|
req = {"input": [{"role": "user", "content": [{"type": "input_text", "text": "bak"}, dict(img)]}]}
|
||||||
|
self.assertEqual(resim_context.resimleri_sadelestir(req), (0, 0))
|
||||||
|
self.assertEqual(req["input"][0]["content"][1]["type"], "input_image")
|
||||||
|
|
||||||
|
|
||||||
|
class Katalog(unittest.TestCase):
|
||||||
|
def test_names_stay_distinct_when_the_id_tail_collides_too(self):
|
||||||
|
names = ide_catalog.adlandir([("a-ortakson", "Ad"), ("b-ortakson", "Ad"), ("c-ortakson", "Ad")], dolu={"ad"})
|
||||||
|
self.assertEqual(len({v.lower() for v in names.values()}), 3)
|
||||||
|
|
||||||
|
def test_an_original_listed_as_extra_keeps_its_place(self):
|
||||||
|
models = [{"slug": "gpt-6-sol", "display_name": "GPT-6-Sol", "priority": 1},
|
||||||
|
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2},
|
||||||
|
{"slug": "ide/a", "display_name": "Aaa", "priority": 0}]
|
||||||
|
extras = [{"slug": "gpt-6-astra"}, {"slug": "ide/a", "kind": "combo"}]
|
||||||
|
ide_catalog.sirala_codex(models, extras, {"gpt-6-sol", "gpt-6-astra"})
|
||||||
|
self.assertEqual([(m["slug"], m["priority"]) for m in models],
|
||||||
|
[("gpt-6-sol", 1), ("gpt-6-astra", 2), ("ide/a", 3)])
|
||||||
|
|
||||||
|
|
||||||
|
class Sahte:
|
||||||
|
"""A handler stand-in: real bridge methods, recorded output."""
|
||||||
|
|
||||||
|
def __init__(self, headers=None, command="POST"):
|
||||||
|
self.headers = headers or {}
|
||||||
|
self.command = command
|
||||||
|
self.path = "/backend-api/codex/responses"
|
||||||
|
self.wfile = io.BytesIO()
|
||||||
|
self.status = None
|
||||||
|
self.sent = []
|
||||||
|
self._passthrough = Mock()
|
||||||
|
self._yerel_yanit = Mock()
|
||||||
|
self._send_json = Mock()
|
||||||
|
self._stream = Mock()
|
||||||
|
self._tasma_ogren = Mock()
|
||||||
|
self._aracsiz_ogren = Mock()
|
||||||
|
|
||||||
|
def send_response(self, status):
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
def send_header(self, k, v):
|
||||||
|
self.sent.append((k, v))
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Zincir(unittest.TestCase):
|
||||||
|
"""_responses + _model_komutu with a conversation key, the way Codex sends it."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.env = build(top("nokta_mesaji_mi", "NOKTA_ISARETLERI", "is_timeout", "kopma_mi", "flatten_request",
|
||||||
|
"claude_sonu_duzelt") + [method("_responses"), method("_model_komutu")], {
|
||||||
|
"json": json, "re": __import__("re"), "os": __import__("os"), "copy": copy, "http": __import__("http.client"),
|
||||||
|
"time": types.SimpleNamespace(sleep=lambda s: None, time=time.time),
|
||||||
|
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
|
||||||
|
error=urllib.error),
|
||||||
|
"decompress": lambda body, enc: body, "MODEL_MAP": {}, "ROUTES": {}, "CFG": {"model_bilgi": False}, "log": lambda m: None,
|
||||||
|
"model_gecis": model_gecis, "YONLENDIRME": model_gecis.Yonlendirme(None), "bilinen_modeller": lambda: MODELS,
|
||||||
|
"model_adi": lambda slug: dict(MODELS).get(slug, slug), "is_hatti": is_hatti, "GORULEN_ISTEMCI": set(),
|
||||||
|
"yarim": yarim, "ozet_kopru": ozet_kopru, "OZET_FRENI": ozet_kopru.DonguFreni(), "pencere": pencere,
|
||||||
|
"fix_tool_ids": lambda req: 0, "drop_foreign_items": lambda req: 0, "IDE_BILGI": {}, "EXTRA_MODELS": [],
|
||||||
|
"arac_semalarini_duzelt": lambda req: 0, "SseRewriter": Mock(), "DEBUG": False, "UA": "test", "RETRIES": 6,
|
||||||
|
"UPSTREAM_IDLE_TIMEOUT": 1, "NO_RETRY_PATTERNS": [], "pano_blogunu_ulastir": lambda req: (0, ""),
|
||||||
|
"resimleri_sadelestir": lambda req, gorsel=False: (0, 0),
|
||||||
|
})
|
||||||
|
Sahte._responses = self.env["_responses"]
|
||||||
|
Sahte._model_komutu = self.env["_model_komutu"]
|
||||||
|
|
||||||
|
def send(self, model, items, key="sohbet-1", path="/responses"):
|
||||||
|
fake = Sahte()
|
||||||
|
body = json.dumps({"model": model, "prompt_cache_key": key, "input": items}).encode()
|
||||||
|
fake._responses(1, path, body)
|
||||||
|
return fake, body
|
||||||
|
|
||||||
|
def test_mid_turn_note_reaches_the_subscription_model_behind_hook_messages(self):
|
||||||
|
items = [user("işe başla"), CALL, OUT, user("modeli sol yap"), developer("k1"), developer("k2"), developer("k3")]
|
||||||
|
fake, body = self.send("gpt-6-astra", items)
|
||||||
|
fake._yerel_yanit.assert_not_called()
|
||||||
|
args, kwargs = fake._passthrough.call_args
|
||||||
|
self.assertTrue(kwargs["rewritten"])
|
||||||
|
self.assertNotEqual(args[2], body)
|
||||||
|
sent = json.loads(args[2])
|
||||||
|
self.assertIn("ad belirsizdi", sent["input"][3]["content"][0]["text"])
|
||||||
|
self.assertEqual(sent["model"], "gpt-6-astra")
|
||||||
|
|
||||||
|
def test_idle_switch_says_how_to_return_and_routes_the_next_request(self):
|
||||||
|
fake, _ = self.send("gpt-6-sol", [user("modeli gpt 6 astra yap")])
|
||||||
|
answer = fake._yerel_yanit.call_args.args[1]
|
||||||
|
self.assertIn("Sohbetin modeli artık GPT-6-Astra", answer)
|
||||||
|
self.assertIn('"modeli GPT-6-Sol yap"', answer)
|
||||||
|
final = {"type": "message", "role": "assistant", "phase": "final_answer", "content": "ok"}
|
||||||
|
fake, _ = self.send("gpt-6-sol", [user("modeli gpt 6 astra yap"), final, user("devam")])
|
||||||
|
sent = json.loads(fake._passthrough.call_args.args[2])
|
||||||
|
self.assertEqual(sent["model"], "gpt-6-astra")
|
||||||
|
self.assertIn("GPT-6-Astra olarak değiştirdi", sent["input"][0]["content"][0]["text"])
|
||||||
|
back, _ = self.send("gpt-6-sol", [user("modeli gpt 6 astra yap"), final, user("modeli gpt 6 sol yap")])
|
||||||
|
self.assertNotIn("dönmek için", back._yerel_yanit.call_args.args[1]) # already back on the picked model
|
||||||
|
|
||||||
|
def test_a_refused_switch_is_not_told_as_done_after_compaction(self):
|
||||||
|
items = [user("modeli opus 5 5 yap"), {"type": "compaction_trigger"}] # the ide/ route is closed here
|
||||||
|
fake, body = self.send("gpt-6-sol", items)
|
||||||
|
self.assertEqual(fake._passthrough.call_args.args[2], body)
|
||||||
|
|
||||||
|
def test_lone_dot_answer_says_selected_not_ready(self):
|
||||||
|
self.env["CFG"]["nokta_karsila"] = True
|
||||||
|
fake, _ = self.send("gpt-6-sol", [user(".")])
|
||||||
|
self.assertIn("GPT-6-Sol seçildi", fake._yerel_yanit.call_args.args[1])
|
||||||
|
self.assertNotIn("hazır", fake._yerel_yanit.call_args.args[1])
|
||||||
|
|
||||||
|
def test_old_router_name_without_a_route_is_refused_locally(self):
|
||||||
|
fake, _ = self.send("9r/cc/claude-opus-5-5", [user("selam")])
|
||||||
|
self.assertEqual(fake._send_json.call_args.args[0], 503)
|
||||||
|
fake._passthrough.assert_not_called()
|
||||||
|
|
||||||
|
def test_a_route_removed_while_the_request_is_prepared_gives_503(self):
|
||||||
|
self.env["MODEL_MAP"]["ide/x"] = ("aiturk-ide", "x", None)
|
||||||
|
fake, _ = self.send("ide/x", [user("selam")])
|
||||||
|
self.assertEqual(fake._send_json.call_args.args[0], 503)
|
||||||
|
self.env["urllib"].request.urlopen.assert_not_called()
|
||||||
|
|
||||||
|
def external(self, error):
|
||||||
|
self.env["ROUTES"]["r"] = {"url": "https://example.invalid/v1/responses", "key": "test-only"}
|
||||||
|
self.env["MODEL_MAP"].update({"x/combo": ("r", "Takim", None), "x/tek": ("r", "cc/claude", None)})
|
||||||
|
self.env["urllib"].request.urlopen.side_effect = error
|
||||||
|
|
||||||
|
def test_combo_dropped_after_sending_is_not_resent_but_a_single_model_is(self):
|
||||||
|
import http.client
|
||||||
|
self.external(http.client.RemoteDisconnected("closed"))
|
||||||
|
self.send("x/combo", [user("selam")])
|
||||||
|
self.assertEqual(self.env["urllib"].request.urlopen.call_count, 1)
|
||||||
|
self.env["urllib"].request.urlopen.reset_mock()
|
||||||
|
self.external([urllib.error.URLError(ConnectionResetError("reset")), Mock(status=200)])
|
||||||
|
fake, _ = self.send("x/tek", [user("selam")])
|
||||||
|
self.assertEqual(self.env["urllib"].request.urlopen.call_count, 2)
|
||||||
|
fake._stream.assert_called_once()
|
||||||
|
self.env["urllib"].request.urlopen.reset_mock()
|
||||||
|
self.external([urllib.error.URLError(ConnectionRefusedError("refused")), Mock(status=200)])
|
||||||
|
self.send("x/combo", [user("selam")]) # never connected: safe to retry
|
||||||
|
self.assertEqual(self.env["urllib"].request.urlopen.call_count, 2)
|
||||||
|
|
||||||
|
def test_error_body_reaches_the_client_whole_with_retry_after(self):
|
||||||
|
whole = json.dumps({"error": {"message": "limit " + "x" * 900, "code": "rate_limit_exceeded"}}).encode()
|
||||||
|
self.external(urllib.error.HTTPError("https://example.invalid", 429, "x", {"Retry-After": "17"}, io.BytesIO(whole)))
|
||||||
|
fake, _ = self.send("x/tek", [user("selam")])
|
||||||
|
self.assertEqual(fake.status, 429)
|
||||||
|
self.assertEqual(fake.wfile.getvalue(), whole)
|
||||||
|
self.assertIn(("Retry-After", "17"), fake.sent)
|
||||||
|
self.assertIn(("Content-Length", str(len(whole))), fake.sent)
|
||||||
|
|
||||||
|
def test_image_in_an_orphan_tool_output_is_removed_before_flattening(self):
|
||||||
|
self.env["resimleri_sadelestir"] = resim_context.resimleri_sadelestir
|
||||||
|
self.env["CFG"]["nine_router_catalog"] = {"enabled": True, "route": "r", "prefix": "9r/"}
|
||||||
|
self.external(None)
|
||||||
|
self.env["ROUTES"]["r"]["flatten_tools"] = True
|
||||||
|
self.env["urllib"].request.urlopen.return_value = Mock(status=200)
|
||||||
|
picture = {"type": "input_image", "image_url": "data:image/png;base64," + "QUJD" * 500}
|
||||||
|
self.send("x/tek", [{"type": "function_call_output", "name": "image_gen", "output": [picture]}, user("devam")])
|
||||||
|
sent = self.env["urllib"].request.urlopen.call_args.args[0].data.decode()
|
||||||
|
self.assertNotIn("QUJDQUJD", sent)
|
||||||
|
self.assertIn("resim", sent)
|
||||||
|
|
||||||
|
|
||||||
|
class Govde(unittest.TestCase):
|
||||||
|
read = build([method("_read")], {})["_read"]
|
||||||
|
|
||||||
|
def test_chunked_upload_cut_short_ends_instead_of_spinning(self):
|
||||||
|
fake = types.SimpleNamespace(headers={"Transfer-Encoding": "chunked"}, rfile=io.BytesIO(b"5\r\nhello\r\n"))
|
||||||
|
self.assertEqual(Govde.read(fake), b"hello")
|
||||||
|
fake = types.SimpleNamespace(headers={"Transfer-Encoding": "chunked"}, rfile=io.BytesIO(b"5\r\nhello\r\n0\r\n\r\n"))
|
||||||
|
self.assertEqual(Govde.read(fake), b"hello")
|
||||||
|
|
||||||
|
|
||||||
|
class Liste(unittest.TestCase):
|
||||||
|
def env(self, **extra):
|
||||||
|
env = {"json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
|
||||||
|
"decompress": lambda raw, enc: raw, "EXTRA_MODELS": [], "IDE_PREFIX": "ide/", "SLIM_CLONES": False,
|
||||||
|
"SLIM_FIELDS": [], "ide_catalog": ide_catalog, "LAST_MODELS": {}, "save_last_models": Mock(),
|
||||||
|
"RESP_HOP": {"content-length", "content-encoding"}, "REQ_HOP": {"host"}, "CHATGPT": "https://example.invalid",
|
||||||
|
"UPSTREAM_IDLE_TIMEOUT": 1,
|
||||||
|
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
|
||||||
|
error=urllib.error)}
|
||||||
|
env.update(extra)
|
||||||
|
return build([method("_models"), method("_passthrough"), method("_son_listeyi_ver")], env)
|
||||||
|
|
||||||
|
UPSTREAM = {"models": [{"slug": "gpt-6-sol", "display_name": "GPT-6-Sol", "priority": 1},
|
||||||
|
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2}]}
|
||||||
|
|
||||||
|
def test_gzip_answer_is_still_cached_plain_and_originals_keep_name_and_place(self):
|
||||||
|
extras = [{"slug": "gpt-6-astra", "display_name": "Baska Ad"}, {"slug": "ide/a", "display_name": "Aaa", "kind": "combo"}]
|
||||||
|
env = self.env(EXTRA_MODELS=extras, CFG={"display_map": {"gpt-6-sol": "Yeni Ad"}})
|
||||||
|
fake = Sahte({"Accept-Encoding": "gzip", "chatgpt-account-id": "hesap"}, "GET")
|
||||||
|
resp = types.SimpleNamespace(read=lambda: json.dumps(self.UPSTREAM).encode(), headers={"ETag": "x"})
|
||||||
|
env["_models"](fake, resp, "[t]")
|
||||||
|
listed = json.loads(gzip.decompress(fake.wfile.getvalue()))["models"]
|
||||||
|
self.assertEqual([(m["slug"], m["display_name"]) for m in listed],
|
||||||
|
[("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("ide/a", "Aaa")])
|
||||||
|
cached = env["LAST_MODELS"]["hesap"]
|
||||||
|
self.assertEqual(json.loads(cached["body"])["models"], listed) # plain JSON, usable as a fallback
|
||||||
|
self.assertNotIn("Content-Encoding", dict(cached["headers"]))
|
||||||
|
self.assertIn(("Content-Encoding", "gzip"), fake.sent)
|
||||||
|
env["save_last_models"].assert_called_once()
|
||||||
|
|
||||||
|
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})
|
||||||
|
env["urllib"].request.urlopen.side_effect = urllib.error.URLError(OSError("no network"))
|
||||||
|
fake = Sahte({"chatgpt-account-id": "hesap"}, "GET")
|
||||||
|
fake._son_listeyi_ver = types.MethodType(env["_son_listeyi_ver"], fake)
|
||||||
|
env["_passthrough"](fake, 1, "/backend-api/codex/models?client_version=1", b"")
|
||||||
|
self.assertEqual((fake.status, fake.wfile.getvalue()), (200, b'{"models": []}'))
|
||||||
|
other = Sahte({"chatgpt-account-id": "baska"}, "GET")
|
||||||
|
other._son_listeyi_ver = types.MethodType(env["_son_listeyi_ver"], other)
|
||||||
|
env["_passthrough"](other, 1, "/backend-api/codex/models", b"")
|
||||||
|
self.assertEqual(other._send_json.call_args.args[0], 502)
|
||||||
|
|
||||||
|
|
||||||
|
class Kucukler(unittest.TestCase):
|
||||||
|
def test_log_line_has_the_date_and_no_token_tail_and_status_reports_the_code(self):
|
||||||
|
self.assertIn('time.strftime("%Y-%m-%d %H:%M:%S ") + msg', SOURCE)
|
||||||
|
self.assertNotIn("token=..", SOURCE)
|
||||||
|
self.assertNotIn("DISPLAY_MAP", SOURCE)
|
||||||
|
self.assertIn('"kod": KOD', SOURCE)
|
||||||
|
|
||||||
|
def test_responses_path_with_a_query_is_still_routed(self):
|
||||||
|
env = build([method("_handle")], {"lock": __import__("threading").Lock(), "seq": [0], "DEBUG": False,
|
||||||
|
"log": lambda m: None})
|
||||||
|
fake = Sahte({})
|
||||||
|
fake.path = "/backend-api/codex/responses?stream=true"
|
||||||
|
fake._read = lambda: b"{}"
|
||||||
|
fake._responses = Mock()
|
||||||
|
env["_handle"](fake)
|
||||||
|
fake._responses.assert_called_once()
|
||||||
|
fake._passthrough.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -84,7 +84,7 @@ class Secim(unittest.TestCase):
|
|||||||
("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"),
|
("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"),
|
("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"),
|
||||||
("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"),
|
("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"),
|
("9R · nano/qwen/qwen3.8-27b", "nano/qwen3.8-27b"), ("9R · ag/gemini", "ag/gemini"),
|
||||||
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
|
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
|
||||||
self.assertEqual(c.kisa_ad(tam), beklenen, tam)
|
self.assertEqual(c.kisa_ad(tam), beklenen, tam)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""The "send larger work to AITURK" instruction reaches only Codex Desktop, and only when a send command exists."""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import is_hatti as h
|
||||||
|
|
||||||
|
|
||||||
|
class Hat(unittest.TestCase):
|
||||||
|
def test_only_the_desktop_app_is_recognised(self):
|
||||||
|
self.assertTrue(h.masaustu_mu("Codex Desktop"))
|
||||||
|
for other in ("codex-tui", "codex_exec", "codex_cli_rs", "", None, "Codex Desktop Helper"):
|
||||||
|
self.assertFalse(h.masaustu_mu(other), other)
|
||||||
|
|
||||||
|
def test_no_instruction_when_switched_off_or_without_a_send_command(self):
|
||||||
|
self.assertIsNone(h.talimat(None))
|
||||||
|
self.assertIsNone(h.talimat({}))
|
||||||
|
self.assertIsNone(h.talimat({"enabled": True, "komut": " "}))
|
||||||
|
self.assertIsNone(h.talimat({"enabled": False, "komut": "aiturk gonder"}))
|
||||||
|
self.assertIsNone(h.talimat({"enabled": False}, "C:/a/aiturk.exe", "s1"))
|
||||||
|
text = h.talimat({"enabled": True, "komut": "aiturk gonder --dosya is.md"})
|
||||||
|
self.assertIn("aiturk gonder --dosya is.md", text)
|
||||||
|
self.assertIn("sen yap", text)
|
||||||
|
self.assertEqual(h.talimat({"enabled": True, "komut": "x", "talimat": "ozel {komut}"}), "ozel x")
|
||||||
|
|
||||||
|
def test_on_by_default_once_a_send_command_exists_on_this_computer(self):
|
||||||
|
text = h.talimat(None, "C:/Program/aiturk.exe", "019a-bc_d")
|
||||||
|
self.assertIn('"C:/Program/aiturk.exe" is --sohbet 019a-bc_d --dosya', text)
|
||||||
|
self.assertIn('"C:/Program/aiturk.exe" is --dosya', h.talimat({}, "C:/Program/aiturk.exe", ""))
|
||||||
|
# Sohbet kimligi komut satirina yazilir: kabuk karakteri tasiyamaz.
|
||||||
|
self.assertIn("--sohbet abrm-rf ", h.komut_satiri("x", 'a"b; rm -rf /'))
|
||||||
|
# Ayardaki komut bulunanin onune gecer.
|
||||||
|
self.assertIn("elle", h.talimat({"komut": "elle"}, "C:/Program/aiturk.exe"))
|
||||||
|
|
||||||
|
def test_the_first_candidate_that_knows_the_command_is_used(self):
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
eski = os.path.join(d, "TurkServis", "AITURK", "program", "bin", "aiturk.cmd")
|
||||||
|
yeni = os.path.join(d, "TurkServis", "AITURK-is-hatti", "Scripts", "aiturk.exe")
|
||||||
|
for yol in (eski, yeni):
|
||||||
|
os.makedirs(os.path.dirname(yol))
|
||||||
|
open(yol, "w").close()
|
||||||
|
ortam = {"LOCALAPPDATA": d}
|
||||||
|
self.assertEqual(h.adaylar(ortam)[0], eski)
|
||||||
|
self.assertEqual(h.komut_bul(ortam, destek=lambda y: y == yeni), yeni)
|
||||||
|
self.assertEqual(h.komut_bul(ortam, destek=lambda y: True), eski)
|
||||||
|
self.assertIsNone(h.komut_bul(ortam, destek=lambda y: False))
|
||||||
|
|
||||||
|
class Sonuc:
|
||||||
|
returncode = 2
|
||||||
|
self.assertFalse(h.destekliyor(eski, calistir=lambda *a, **k: Sonuc()))
|
||||||
|
Sonuc.returncode = 0
|
||||||
|
self.assertTrue(h.destekliyor(eski, calistir=lambda *a, **k: Sonuc()))
|
||||||
|
self.assertFalse(h.destekliyor(os.path.join(d, "yok.exe"), calistir=lambda *a, **k: Sonuc()))
|
||||||
|
|
||||||
|
def patlar(*a, **k):
|
||||||
|
raise OSError("x")
|
||||||
|
self.assertFalse(h.destekliyor(eski, calistir=patlar))
|
||||||
|
|
||||||
|
def test_instruction_is_put_first_once_and_never_repeated(self):
|
||||||
|
req = {"input": [{"type": "message", "role": "user", "content": "selam"}]}
|
||||||
|
text = h.talimat({"enabled": True, "komut": "aiturk gonder"})
|
||||||
|
self.assertTrue(h.ekle(req, text))
|
||||||
|
self.assertEqual((req["input"][0]["role"], len(req["input"])), ("developer", 2))
|
||||||
|
self.assertFalse(h.ekle(req, text))
|
||||||
|
self.assertEqual(len(req["input"]), 2)
|
||||||
|
self.assertFalse(h.ekle({"input": "duz metin"}, text))
|
||||||
|
self.assertFalse(h.ekle(req, None))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Bridge 7: tool-less models are learned, flat gateway errors are translated, instructions are never trimmed."""
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pencere
|
||||||
|
|
||||||
|
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
|
||||||
|
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti",
|
||||||
|
"sema_duzelt", "arac_semalarini_duzelt"}
|
||||||
|
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
|
||||||
|
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI", "NOKTA_ISARETLERI"} for t in node.targets):
|
||||||
|
nodes.insert(0, node)
|
||||||
|
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
|
||||||
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||||
|
|
||||||
|
|
||||||
|
def feed(event, **kw):
|
||||||
|
adapter = env["SseRewriter"](set(), **kw)
|
||||||
|
raw = b"data: " + json.dumps(event).encode() + b"\n\n"
|
||||||
|
return [json.loads(line[6:]) for line in (adapter.feed(raw) + adapter.flush()).decode().splitlines()
|
||||||
|
if line.startswith("data: ")]
|
||||||
|
|
||||||
|
|
||||||
|
class Ek(unittest.TestCase):
|
||||||
|
def test_model_without_tool_calls_is_reported_and_remembered(self):
|
||||||
|
seen = []
|
||||||
|
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
|
||||||
|
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
|
||||||
|
out = feed(flat, hedef="m", aracsiz=seen.append)
|
||||||
|
# A permanent error ends the turn with a visible answer instead of a silent retry.
|
||||||
|
self.assertEqual([e["type"] for e in out], ["response.output_item.added", "response.output_text.delta",
|
||||||
|
"response.output_item.done", "response.completed"])
|
||||||
|
self.assertIn("araç çağrısını desteklemiyor", out[1]["delta"])
|
||||||
|
self.assertEqual(out[2]["item"]["phase"], "final_answer")
|
||||||
|
self.assertEqual(seen, ["m"])
|
||||||
|
home = tempfile.mkdtemp()
|
||||||
|
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
|
||||||
|
self.assertFalse(pencere.ogren_aracsiz(home, "m"))
|
||||||
|
self.assertFalse(pencere.coz("m", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
|
||||||
|
self.assertTrue(pencere.coz("n", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
|
||||||
|
|
||||||
|
def test_flat_overflow_error_is_translated_and_learned(self):
|
||||||
|
seen = []
|
||||||
|
flat = {"type": "error", "code": "bad_request", "message":
|
||||||
|
"Your prompt exceeds the model's context length. Max context tokens: 65536."}
|
||||||
|
out = feed(flat, hedef="m", tasma=lambda hedef, metin: seen.append(pencere.sinir_oku(metin)))
|
||||||
|
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
|
||||||
|
self.assertEqual(seen, [65536])
|
||||||
|
self.assertEqual(feed({"type": "error", "code": "server_error", "message": "upstream 503"})[0]["type"], "error")
|
||||||
|
|
||||||
|
def test_guard_never_trims_instruction_messages(self):
|
||||||
|
big = "KURAL " * 40000
|
||||||
|
req = {"input": [
|
||||||
|
{"type": "message", "role": "developer", "content": [{"type": "input_text", "text": big}]},
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "eski " * 40000}]},
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "soru"}]}]}
|
||||||
|
pencere.sigdir(req, 65536)
|
||||||
|
self.assertEqual(req["input"][0]["content"][0]["text"], big)
|
||||||
|
self.assertIn("çıkarıldı (köprü)", req["input"][1]["content"][0]["text"])
|
||||||
|
|
||||||
|
|
||||||
|
class Sema(unittest.TestCase):
|
||||||
|
def test_top_level_union_becomes_one_object_schema(self):
|
||||||
|
req = {"tools": [
|
||||||
|
{"type": "function", "name": "a", "parameters": {"oneOf": [
|
||||||
|
{"type": "object", "properties": {"x": {"type": "string"}, "k": {"type": "string"}}, "required": ["x", "k"]},
|
||||||
|
{"type": "object", "properties": {"y": {"type": "integer"}, "k": {"type": "string"}}, "required": ["y", "k"]}]}},
|
||||||
|
{"type": "function", "name": "b", "parameters": {"type": "object", "properties": {"z": {"anyOf": [{"type": "string"}, {"type": "null"}]}}}},
|
||||||
|
{"type": "function", "name": "c", "parameters": {"allOf": [
|
||||||
|
{"type": "object", "properties": {"p": {"type": "string"}}, "required": ["p"]},
|
||||||
|
{"type": "object", "properties": {"q": {"type": "string"}}, "required": ["q"]}]}}]}
|
||||||
|
self.assertEqual(env["arac_semalarini_duzelt"](req), 2)
|
||||||
|
a, b, c = (t["parameters"] for t in req["tools"])
|
||||||
|
self.assertEqual((a["type"], sorted(a["properties"]), a["required"]), ("object", ["k", "x", "y"], ["k"]))
|
||||||
|
self.assertNotIn("oneOf", a)
|
||||||
|
self.assertIn("anyOf", b["properties"]["z"]) # nested unions are left alone
|
||||||
|
self.assertEqual((sorted(c["properties"]), c["required"]), (["p", "q"], ["p", "q"]))
|
||||||
|
self.assertEqual(env["arac_semalarini_duzelt"](req), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class Nokta(unittest.TestCase):
|
||||||
|
def user(self, content):
|
||||||
|
return {"type": "message", "role": "user", "content": content}
|
||||||
|
|
||||||
|
def test_only_a_lone_dot_from_the_user_is_answered_locally(self):
|
||||||
|
is_dot = env["nokta_mesaji_mi"]
|
||||||
|
self.assertTrue(is_dot({"input": [self.user([{"type": "input_text", "text": " . "}])]}))
|
||||||
|
self.assertTrue(is_dot({"input": [self.user("x"), self.user(".")]}))
|
||||||
|
for other in ("..", "?", "Selam", "", "1."):
|
||||||
|
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": other}])]}), other)
|
||||||
|
self.assertFalse(is_dot({"input": [self.user("."), {"type": "function_call_output", "call_id": "c", "output": "x"}]}))
|
||||||
|
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": "."}, {"type": "input_image", "image_url": "x"}])]}))
|
||||||
|
self.assertFalse(is_dot({"input": [{"type": "message", "role": "assistant", "content": "."}]}))
|
||||||
|
self.assertFalse(is_dot({"input": []}))
|
||||||
|
|
||||||
|
def test_local_answer_is_a_complete_response_with_a_final_answer(self):
|
||||||
|
raw = env["hazir_yaniti"]("ide/x", "cc/opus-5-5 hazır.").decode()
|
||||||
|
events = [json.loads(line[6:]) for line in raw.splitlines() if line.startswith("data: ")]
|
||||||
|
self.assertEqual([e["type"] for e in events], ["response.created", "response.output_item.added",
|
||||||
|
"response.output_text.delta", "response.output_item.done", "response.completed"])
|
||||||
|
done = events[-1]["response"]
|
||||||
|
self.assertEqual(done["status"], "completed")
|
||||||
|
self.assertEqual(done["output"][0]["phase"], "final_answer")
|
||||||
|
self.assertEqual(done["output"][0]["content"][0]["text"], "cc/opus-5-5 hazır.")
|
||||||
|
self.assertEqual(done["usage"]["total_tokens"], 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -55,9 +55,87 @@ class MemberCatalog(unittest.TestCase):
|
|||||||
for m in models if m["slug"].startswith("ide/")]
|
for m in models if m["slug"].startswith("ide/")]
|
||||||
c.sirala_codex(models, extras)
|
c.sirala_codex(models, extras)
|
||||||
self.assertEqual([m["slug"] for m in models],
|
self.assertEqual([m["slug"] for m in models],
|
||||||
["ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus", "gpt-z"])
|
["gpt-z", "ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus"])
|
||||||
self.assertEqual([m["priority"] for m in models], list(range(5)))
|
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")
|
||||||
|
# 9Router aliases: cc = Claude Code, cx = OpenAI Codex, ag = Antigravity.
|
||||||
|
self.assertEqual(c.aciklama("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"), "Tek model · Codex")
|
||||||
|
self.assertEqual(c.aciklama("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"), "Tek model · Antigravity")
|
||||||
|
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))
|
||||||
|
|
||||||
|
def test_picker_names_are_combo_name_or_short_provider_slash_model(self):
|
||||||
|
# User decision, 10 Oct 2026: no "9R" mark; combos by name; single models as provider/model.
|
||||||
|
rows = [("Takim-Hizli", "Takim-Hizli", "combo"),
|
||||||
|
("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"),
|
||||||
|
("9router-cc-claude-opus-4-8", "9R · cc/claude-opus-4-8", "model"),
|
||||||
|
("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"),
|
||||||
|
("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"),
|
||||||
|
("9router-nano-z-ai-glm-5.3-flash-uncensored", "9R · nano/glm-5.3-flash-uncensored", "model"),
|
||||||
|
("agnes-agnes-2.0-flash", "Agnes 2.0 Flash", "model"),
|
||||||
|
("agnes-agnes-2.0-flash-high", "agnes-2.0-flash-high", "model"),
|
||||||
|
("bekci-bekci", "bekci", "model"), ("bekci-kurnaz", "kurnaz", "model"),
|
||||||
|
("hf-deepseek-ai-DeepSeek-V3", "hf/DeepSeek V3", "model"),
|
||||||
|
("no-think-9router-cc-claude-opus-5-5", "no-think/claude-opus-5-5", "model"),
|
||||||
|
("no-think-9router-ag-claude-opus-4-6-thinking", "no-think/claude-opus-4-6-thinking", "model")]
|
||||||
|
adlar = c.adlandir([(k, a) for k, a, _ in rows], turler={k: t for k, _, t in rows})
|
||||||
|
self.assertEqual([adlar[k] for k, _, _ in rows],
|
||||||
|
["Takim-Hizli", "cc/opus-5-5", "cc/opus-4-8", "cx/gpt-6-astra", "agy/gemini-3.8-flash-high",
|
||||||
|
"nano/glm-5.3-flash-uncensored", "agnes/agnes-2.0-flash", "agnes/agnes-2.0-flash-high",
|
||||||
|
"bekci/bekci", "bekci/kurnaz", "hf/DeepSeek-V3", "cc/opus-5-5 (no-think)",
|
||||||
|
"agy/opus-4-6-thinking (no-think)"])
|
||||||
|
self.assertFalse([a for a in adlar.values() if "9R" in a])
|
||||||
|
# Added models sort after the combos by provider then model.
|
||||||
|
models = [{"slug": "ide/" + k, "display_name": adlar[k], "priority": 0} for k, _, _ in rows]
|
||||||
|
c.sirala_codex(models, [{"slug": "ide/" + k, "kind": t} for k, _, t in rows])
|
||||||
|
gorunen = [m["display_name"] for m in models]
|
||||||
|
self.assertEqual(gorunen[0], "Takim-Hizli")
|
||||||
|
self.assertEqual(gorunen[1:], sorted(gorunen[1:], key=lambda a: tuple(p.casefold() for p in a.partition("/")[::2])))
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""Bridge 8: "modeli X yap" is handled by the bridge; a per-chat route survives until the user picks another model."""
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import model_gecis as g
|
||||||
|
|
||||||
|
MODELS = [("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("gpt-6.1-sol", "GPT-6.1-Sol"),
|
||||||
|
("ide/9router-ag-gemini-3.8-flash-low", "agy/gemini-3.8-flash-low"),
|
||||||
|
("ide/9router-ag-gemini-3.8-flash-high", "agy/gemini-3.8-flash-high"),
|
||||||
|
("ide/9router-cc-claude-opus-5-5", "cc/opus-5-5"), ("ide/9router-cc-claude-opus-5-5-nt", "cc/opus-5-5 (no-think)"),
|
||||||
|
("ide/9router-nano-z-ai-glm-5.3-flash-uncensored", "nano/glm-5.3-flash-uncensored"),
|
||||||
|
("ide/agnes-agnes-2.0-flash", "agnes/agnes-2.0-flash"), ("ide/agnes-agnes-2.5-flash", "agnes/agnes-2.5-flash")]
|
||||||
|
|
||||||
|
|
||||||
|
def user(text):
|
||||||
|
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
|
||||||
|
|
||||||
|
|
||||||
|
def voice(text):
|
||||||
|
return user(f"<realtime_delegation>\n <input>{text}</input>\n <transcript_delta>user: x</transcript_delta>\n</realtime_delegation>")
|
||||||
|
|
||||||
|
|
||||||
|
def pick(text):
|
||||||
|
command = g.komut_coz(text)
|
||||||
|
return g.eslestir(command["ad"], MODELS) if command and command["tur"] == "gec" else command
|
||||||
|
|
||||||
|
|
||||||
|
class Komut(unittest.TestCase):
|
||||||
|
def test_spoken_names_resolve_to_one_model(self):
|
||||||
|
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
|
||||||
|
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
|
||||||
|
("modeli klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
|
||||||
|
("modeli opus 5 5'e geç", "ide/9router-cc-claude-opus-5-5"),
|
||||||
|
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
|
||||||
|
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]:
|
||||||
|
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
|
||||||
|
|
||||||
|
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
|
||||||
|
self.assertEqual(pick("modeli agnes yap"), {"adaylar": ["agnes/agnes-2.0-flash", "agnes/agnes-2.5-flash"]})
|
||||||
|
self.assertIn("adaylar", pick("modeli klod opus yap")) # two Opus entries: ask
|
||||||
|
self.assertIn("adaylar", pick("modeli sol yap")) # GPT-6-Sol or GPT-6.1-Sol: ask
|
||||||
|
self.assertIn("adaylar", pick("modeli gpt 999 astra yap")) # words fit, version does not: ask
|
||||||
|
|
||||||
|
def test_negations_and_general_questions_are_not_commands(self):
|
||||||
|
for text in ("modeli gemini flash low yapma", "modeli değiştirme opus kalsın", "GPT 6 Astra modeli için test yap",
|
||||||
|
"hangi model daha ucuz", "en iyi model hangisi", "modeli seçili dosyaya göre gemini flash low yap"):
|
||||||
|
result = pick(text)
|
||||||
|
self.assertFalse(result and ("slug" in result or result.get("tur") == "sor"), text)
|
||||||
|
self.assertEqual(g.komut_coz("hangi modelsin"), {"tur": "sor"})
|
||||||
|
|
||||||
|
def test_ordinary_sentences_are_not_commands(self):
|
||||||
|
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
|
||||||
|
"hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
|
||||||
|
"model " + "çok uzun bir cümle " * 5 + "yap"):
|
||||||
|
self.assertFalse(pick(text) and "slug" in pick(text), text)
|
||||||
|
self.assertEqual(g.komut_coz("seçili model hangisi"), {"tur": "sor"})
|
||||||
|
self.assertEqual(g.komut_coz("bu chatte ana model kim"), {"tur": "sor"})
|
||||||
|
|
||||||
|
def test_voice_delegation_yields_only_the_spoken_input(self):
|
||||||
|
text, index, spoken = g.son_kullanici_metni({"input": [user("a"), voice("modeli opus yap")]})
|
||||||
|
self.assertEqual((text, index, spoken), ("modeli opus yap", 1, True))
|
||||||
|
self.assertEqual(g.son_kullanici_metni({"input": [user("<skill>x</skill>")]})[0], None)
|
||||||
|
self.assertEqual(g.son_kullanici_metni({"input": [{"type": "function_call_output", "output": "x"}]})[0], None)
|
||||||
|
|
||||||
|
|
||||||
|
class TurOrtasi(unittest.TestCase):
|
||||||
|
def test_message_after_tool_items_is_mid_turn_and_after_final_answer_is_not(self):
|
||||||
|
final = {"type": "message", "role": "assistant", "phase": "final_answer", "content": "ok"}
|
||||||
|
call = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
|
||||||
|
out = {"type": "function_call_output", "call_id": "c", "output": "x"}
|
||||||
|
self.assertFalse(g.tur_ortasi({"input": [user("a"), final, user(".")]}, 2))
|
||||||
|
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, user(".")]}, 3))
|
||||||
|
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, voice("bekliyorum"), user(".")]}, 4))
|
||||||
|
self.assertTrue(g.tur_ortasi({"input": [user("a"), {"type": "message", "role": "assistant", "phase": "commentary",
|
||||||
|
"content": "bakıyorum"}, user(".")]}, 2))
|
||||||
|
self.assertFalse(g.tur_ortasi({"input": [user(".")]}, 0))
|
||||||
|
|
||||||
|
def test_handover_note_replaces_only_the_spoken_input(self):
|
||||||
|
req = {"input": [voice("modeli opus yap")]}
|
||||||
|
g.metni_degistir(req, 0, g.devir_notu("cc/opus-5-5", "GPT-6-Sol"))
|
||||||
|
text = req["input"][0]["content"][0]["text"]
|
||||||
|
self.assertIn("<transcript_delta>", text)
|
||||||
|
self.assertIn("cc/opus-5-5 olarak devam ediyorum", text)
|
||||||
|
self.assertNotIn("modeli opus yap</input>", text)
|
||||||
|
|
||||||
|
|
||||||
|
class Gecmis(unittest.TestCase):
|
||||||
|
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
|
||||||
|
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
|
||||||
|
voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
|
||||||
|
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
|
||||||
|
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
|
||||||
|
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
|
||||||
|
self.assertIn("<transcript_delta>", req["input"][2]["content"][0]["text"])
|
||||||
|
self.assertEqual(req["input"][3]["content"][0]["text"], "modeli gpt 6 astra yap")
|
||||||
|
self.assertEqual(g.gecmisi_temizle(req, MODELS), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class Bilgi(unittest.TestCase):
|
||||||
|
def test_model_is_told_once_that_the_chat_model_changes_by_saying_so(self):
|
||||||
|
req = {"input": [user("selam")]}
|
||||||
|
self.assertTrue(g.bilgi_ekle(req))
|
||||||
|
self.assertEqual(req["input"][0]["role"], "developer")
|
||||||
|
self.assertIn("modeli <ad> yap", req["input"][0]["content"][0]["text"])
|
||||||
|
self.assertFalse(g.bilgi_ekle(req))
|
||||||
|
self.assertEqual(len(req["input"]), 2)
|
||||||
|
self.assertFalse(g.bilgi_ekle({"input": []}))
|
||||||
|
|
||||||
|
def test_a_command_without_a_name_asks_which_model(self):
|
||||||
|
for text in ("modeli değiştir", "başka modele geç", "modeli değiştirir misin"):
|
||||||
|
self.assertEqual(g.komut_coz(text), {"tur": "nasil"}, text)
|
||||||
|
self.assertIsNone(g.komut_coz("modeli değiştirme"))
|
||||||
|
|
||||||
|
|
||||||
|
class Dogal(unittest.TestCase):
|
||||||
|
GECIT = [("ide/9router/ModelARK/dola-seed-2-1-turbo-260628", "9router/ModelARK/dola-seed-2-1-turbo-260628"),
|
||||||
|
("ide/9router/ModelARK/seed-2-0-pro-260328", "9router/ModelARK/seed-2-0-pro-260328")]
|
||||||
|
|
||||||
|
def test_command_without_the_word_model_and_a_spelled_out_name(self):
|
||||||
|
for text in ("Seed iki nokta bir turbo yap", "Evet. Seed, S E E D iki nokta bir turbo yap"):
|
||||||
|
command = g.komut_coz(text)
|
||||||
|
self.assertEqual((command["ad"], command["kesin"]), (["seed", "2", "1", "turbo"], False), text)
|
||||||
|
self.assertEqual(g.eslestir(command["ad"], self.GECIT)["slug"], "ide/9router/ModelARK/dola-seed-2-1-turbo-260628")
|
||||||
|
self.assertTrue(g.komut_coz("Modeli Seed iki nokta bir turbo yap")["kesin"])
|
||||||
|
self.assertEqual(g.eslestir(g.komut_coz("gemini flash low'a geç")["ad"], MODELS)["slug"],
|
||||||
|
"ide/9router-ag-gemini-3.8-flash-low")
|
||||||
|
|
||||||
|
def test_ordinary_requests_ending_in_a_verb_are_not_commands(self):
|
||||||
|
for text in ("test yap", "kahve yap", "yapma"):
|
||||||
|
self.assertIsNone(g.komut_coz(text), text)
|
||||||
|
for text in ("şu dosyayı düzelt ve commit yap", "rapor hazırla gemini flash low ile karşılaştırma yap"):
|
||||||
|
command = g.komut_coz(text)
|
||||||
|
self.assertFalse(command["kesin"])
|
||||||
|
self.assertFalse((g.eslestir(command["ad"], MODELS) or {}).get("slug"), text)
|
||||||
|
|
||||||
|
|
||||||
|
class Yon(unittest.TestCase):
|
||||||
|
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
y = g.Yonlendirme(home)
|
||||||
|
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
|
||||||
|
self.assertEqual(g.Yonlendirme(home).al("chat", "gpt-6-sol", simdi=1001), "ide/x") # survives a restart
|
||||||
|
self.assertIsNone(y.al("other", "gpt-6-sol", simdi=1001))
|
||||||
|
self.assertIsNone(y.al("chat", "gpt-6-astra", simdi=1002)) # user picked another
|
||||||
|
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1003)) # and it stays cleared
|
||||||
|
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
|
||||||
|
y.koy("chat", "gpt-6-sol", "gpt-6-sol", simdi=1001) # back to the picked one
|
||||||
|
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1002))
|
||||||
|
y.koy("old", "a", "b", simdi=1000)
|
||||||
|
self.assertIsNone(y.al("old", "a", simdi=1000 + g.OMUR_SN + 1))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -7,12 +7,13 @@ import unittest
|
|||||||
|
|
||||||
|
|
||||||
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
|
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
|
||||||
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter"}
|
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter", "phase_damgala",
|
||||||
|
"asistan_mesaji_mi"}
|
||||||
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
||||||
env = {"json": json, "log": lambda message: None}
|
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": __import__("pencere")}
|
||||||
for node in tree.body:
|
for node in tree.body:
|
||||||
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
|
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
|
||||||
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES"} for t in node.targets):
|
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
|
||||||
nodes.insert(0, node)
|
nodes.insert(0, node)
|
||||||
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||||
|
|
||||||
@@ -62,6 +63,27 @@ class ModelSwitch(unittest.TestCase):
|
|||||||
self.assertEqual(rewritten["item"]["type"], "custom_tool_call")
|
self.assertEqual(rewritten["item"]["type"], "custom_tool_call")
|
||||||
self.assertEqual(rewritten["item"]["input"], "remember-4827")
|
self.assertEqual(rewritten["item"]["input"], "remember-4827")
|
||||||
self.assertEqual(rewritten["item"]["call_id"], "call_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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -109,6 +109,35 @@ class Akis(unittest.TestCase):
|
|||||||
a = o.OzetAkisi()
|
a = o.OzetAkisi()
|
||||||
self.assertEqual(o.ozet_metni(coz(a.feed(ham))[1]["item"]), "ozet metni")
|
self.assertEqual(o.ozet_metni(coz(a.feed(ham))[1]["item"]), "ozet metni")
|
||||||
|
|
||||||
|
def test_anormal_kisa_ozet_gecmisin_yerine_konmaz(self):
|
||||||
|
# Esik yalniz bozuk sayilacak kadar kisa ozeti yakalar. 10 Ekim 2026 olcumu: 646.955 baytlik
|
||||||
|
# sohbetin dogru ozeti 1501-1701 karakterdi ve boyuta oranli eski esik onu reddedip sohbeti kilitledi.
|
||||||
|
self.assertEqual(o.en_az_karakter(1018151), 300)
|
||||||
|
self.assertEqual(o.en_az_karakter(646955), 300)
|
||||||
|
self.assertEqual(o.en_az_karakter(100000), 50)
|
||||||
|
self.assertEqual(o.en_az_karakter(0), 0)
|
||||||
|
self.assertEqual(o.en_az_karakter(1018151, {"en_az_tavan": 1500, "en_az_oran": 250}), 1500)
|
||||||
|
for gercek_ozet in (1501, 1648, 1701):
|
||||||
|
self.assertGreater(gercek_ozet, o.en_az_karakter(646955))
|
||||||
|
ham = sse({"type": "response.created", "response": {"id": "r1"}},
|
||||||
|
{"type": "response.output_item.done", "item": self._mesaj("k" * 120)},
|
||||||
|
{"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" * 1501, o.en_az_karakter(646955)), ("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):
|
def test_metin_yoksa_compaction_uretilmez_hata_gorunur(self):
|
||||||
ham = sse({"type": "response.created", "response": {}},
|
ham = sse({"type": "response.created", "response": {}},
|
||||||
{"type": "response.output_item.done", "item": {"type": "function_call", "name": "x"}},
|
{"type": "response.output_item.done", "item": {"type": "function_call", "name": "x"}},
|
||||||
@@ -133,11 +162,22 @@ class Akis(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class Fren(unittest.TestCase):
|
class Fren(unittest.TestCase):
|
||||||
def test_ayni_sohbette_ust_uste_ozet_durdurulur(self):
|
def test_tight_loop_is_stopped_but_spaced_summaries_are_not(self):
|
||||||
f = o.DonguFreni(sinir=3, pencere_sn=600)
|
f = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
|
||||||
|
# Summary after summary with no normal request in between: a real loop.
|
||||||
self.assertEqual([f.izin("sohbet-a", t) for t in (0, 10, 20, 30, 40)], [True, True, True, False, False])
|
self.assertEqual([f.izin("sohbet-a", t) for t in (0, 10, 20, 30, 40)], [True, True, True, False, False])
|
||||||
self.assertTrue(f.izin("sohbet-b", 40)) # baska sohbet etkilenmez
|
self.assertTrue(f.izin("sohbet-b", 40)) # another conversation is unaffected
|
||||||
self.assertTrue(f.izin("sohbet-a", 700)) # pencere gecince yeniden izin
|
# A normal request between summaries (small-window model compacting every turn) keeps working.
|
||||||
|
g = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
|
||||||
|
izinler = []
|
||||||
|
for t in range(0, 70, 10):
|
||||||
|
izinler.append(g.izin("sohbet-c", t))
|
||||||
|
g.normal("sohbet-c")
|
||||||
|
self.assertEqual(izinler, [True] * 7)
|
||||||
|
self.assertTrue(g.izin("sohbet-c", 70)) # the 8th in the window
|
||||||
|
g.normal("sohbet-c")
|
||||||
|
self.assertFalse(g.izin("sohbet-c", 80)) # the total cap still ends a slow loop
|
||||||
|
self.assertTrue(g.izin("sohbet-c", 700)) # and lifts once the window passes
|
||||||
self.assertIn("Dongu durduruldu", o.DONGU_MESAJI % 3)
|
self.assertIn("Dongu durduruldu", o.DONGU_MESAJI % 3)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""Bridge version 7: real context windows, phase stamping, overflow errors, images for vision models."""
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import ide_catalog as c
|
||||||
|
import pencere
|
||||||
|
import resim_context
|
||||||
|
|
||||||
|
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
|
||||||
|
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi"}
|
||||||
|
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
|
||||||
|
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
|
||||||
|
nodes.insert(0, node)
|
||||||
|
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
|
||||||
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||||
|
|
||||||
|
NANO = pencere.nano_sadelestir({"data": [
|
||||||
|
{"id": "z-ai/glm-5.3-flash-uncensored", "context_length": 1048576,
|
||||||
|
"capabilities": {"vision": True, "tool_calling": True}},
|
||||||
|
{"id": "mlabonne/NeuralDaredevil-8B-abliterated", "context_length": 8192,
|
||||||
|
"capabilities": {"vision": False, "tool_calling": False}},
|
||||||
|
{"id": "nousresearch/hermes-3-llama-3.1-70b", "context_length": 65536,
|
||||||
|
"capabilities": {"vision": False, "tool_calling": True}},
|
||||||
|
{"id": "broken", "context_length": 0}]})
|
||||||
|
|
||||||
|
|
||||||
|
def sse(*events):
|
||||||
|
return b"".join(b"event: " + e["type"].encode() + b"\ndata: " + json.dumps(e).encode() + b"\n\n" for e in events)
|
||||||
|
|
||||||
|
|
||||||
|
def parse(raw):
|
||||||
|
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
|
||||||
|
|
||||||
|
|
||||||
|
def message(text, ident="msg_1"):
|
||||||
|
return {"type": "message", "id": ident, "role": "assistant", "content": [{"type": "output_text", "text": text}]}
|
||||||
|
|
||||||
|
|
||||||
|
class Windows(unittest.TestCase):
|
||||||
|
def test_provider_data_local_table_and_cap(self):
|
||||||
|
coz = pencere.coz
|
||||||
|
# The provider that carries the request decides; nothing is declared above the summarizer's window.
|
||||||
|
self.assertEqual(coz("9router-nano-z-ai-glm-5.3-flash-uncensored", "model", "nano/z-ai/glm-5.3-flash-uncensored",
|
||||||
|
nano=NANO)["pencere"], pencere.TAVAN)
|
||||||
|
small = coz("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "model",
|
||||||
|
"nano/mlabonne/NeuralDaredevil-8B-abliterated", nano=NANO)
|
||||||
|
self.assertEqual((small["pencere"], small["arac"], small["gorsel"]), (8192, False, False))
|
||||||
|
self.assertEqual(coz("9router-cc-claude-opus-5-5", "model", "cc/claude-opus-5-5")["pencere"], 200000)
|
||||||
|
self.assertEqual(coz("9router-cx-gpt-6-astra", "model", "cx/gpt-6-astra")["pencere"], 272000)
|
||||||
|
self.assertEqual(coz("no-think-9router-cc-claude-opus-5-5", "model", "no-think/claude-opus-5-5")["pencere"], 200000)
|
||||||
|
unknown = coz("agnes-agnes-2.5-flash", "model", "Agnes 2.5 Flash", nano=NANO)
|
||||||
|
self.assertEqual((unknown["pencere"], unknown["kaynak"]), (pencere.VARSAYILAN, "varsayilan"))
|
||||||
|
self.assertNotIn("broken", NANO)
|
||||||
|
|
||||||
|
def test_combo_declares_its_smallest_member(self):
|
||||||
|
combo = {"topology": {"kind": "combo", "strategy": "priority", "members": [
|
||||||
|
{"kind": "model", "model": "cx/gpt-6-astra"},
|
||||||
|
{"kind": "combo", "strategy": "priority", "members": [
|
||||||
|
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
|
||||||
|
{"kind": "model", "model": "nano/nousresearch/hermes-3-llama-3.1-70b"}]}]}}
|
||||||
|
self.assertEqual(pencere.coz("X", "combo", "X", combo, NANO)["pencere"], 65536)
|
||||||
|
self.assertEqual(pencere.coz("X", "combo", "X", None, NANO)["pencere"], pencere.VARSAYILAN)
|
||||||
|
|
||||||
|
def test_limit_learned_from_the_target_error_only_shrinks(self):
|
||||||
|
home = tempfile.mkdtemp()
|
||||||
|
text = "This model's maximum context length is 32768 tokens. However, you requested 51234 tokens"
|
||||||
|
self.assertTrue(pencere.tasma_mi(text))
|
||||||
|
self.assertEqual(pencere.sinir_oku(text), 32768)
|
||||||
|
self.assertEqual(pencere.sinir_oku("prompt is too long: 3040251 tokens > 1000000 maximum"), 1000000)
|
||||||
|
gateway = ("Your prompt exceeds the model's context length. Estimated input tokens: 171016. "
|
||||||
|
"Max context tokens: 65536.")
|
||||||
|
self.assertTrue(pencere.tasma_mi(gateway))
|
||||||
|
self.assertEqual(pencere.sinir_oku(gateway), 65536)
|
||||||
|
self.assertIsNone(pencere.sinir_oku("rate limit exceeded"))
|
||||||
|
self.assertFalse(pencere.tasma_mi("rate limit exceeded"))
|
||||||
|
self.assertTrue(pencere.ogren(home, "m", 32768))
|
||||||
|
self.assertFalse(pencere.ogren(home, "m", 65536)) # a larger number never overrides
|
||||||
|
self.assertEqual(pencere.ogrenilen(home), {"m": 32768})
|
||||||
|
self.assertEqual(pencere.coz("m", "model", "cc/claude-opus-5-5", ogr=pencere.ogrenilen(home))["pencere"], 32768)
|
||||||
|
|
||||||
|
def test_nano_cache_survives_a_network_failure(self):
|
||||||
|
home = tempfile.mkdtemp()
|
||||||
|
fresh = {"data": [{"id": "A/b", "context_length": 16384, "capabilities": {}}]}
|
||||||
|
self.assertEqual(pencere.nano_yukle(home, simdi=1000, getir=lambda: fresh)["a/b"]["pencere"], 16384)
|
||||||
|
|
||||||
|
def fail():
|
||||||
|
raise OSError("offline")
|
||||||
|
later = 1000 + pencere.NANO_OMUR_SN + 5
|
||||||
|
self.assertEqual(pencere.nano_yukle(home, simdi=later, getir=fail)["a/b"]["pencere"], 16384)
|
||||||
|
self.assertEqual(pencere.nano_yukle(tempfile.mkdtemp(), simdi=later, getir=fail), {})
|
||||||
|
self.assertEqual((pencere.kisa(8192), pencere.kisa(131072), pencere.kisa(200000)), ("8K", "128K", "200K"))
|
||||||
|
|
||||||
|
def test_description_line_warns_about_small_window_and_no_tools(self):
|
||||||
|
bilgi = {"pencere": 8192, "arac": False, "gorsel": False}
|
||||||
|
line = c.aciklama("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "9R · nano/NeuralDaredevil-8B-abliterated",
|
||||||
|
"model", None, bilgi)
|
||||||
|
self.assertEqual(line, "NanoGPT · 8K · araçsız") # warnings first; the rest no longer fits in 30
|
||||||
|
self.assertLessEqual(len(line), c.ACIKLAMA_SINIRI)
|
||||||
|
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model", None,
|
||||||
|
{"pencere": 200000, "arac": True, "gorsel": True}), "Tek model · Claude")
|
||||||
|
|
||||||
|
|
||||||
|
class Guard(unittest.TestCase):
|
||||||
|
def req(self, old, last="son soru"):
|
||||||
|
return {"instructions": "talimat", "input": [
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": old}]},
|
||||||
|
{"type": "function_call_output", "call_id": "c", "output": "cikti " * 4000},
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": last}]}]}
|
||||||
|
|
||||||
|
def test_request_that_fits_is_untouched(self):
|
||||||
|
req = self.req("kisa")
|
||||||
|
before = json.dumps(req)
|
||||||
|
self.assertEqual(pencere.sigdir(req, 272000)[:2], (0, True))
|
||||||
|
self.assertEqual(json.dumps(req), before)
|
||||||
|
|
||||||
|
def test_oversized_old_parts_lose_their_middle_and_the_last_user_message_never_does(self):
|
||||||
|
req = self.req("BAS " + "dolgu " * 60000 + " SON", last="yeni " * 3000)
|
||||||
|
kirpilan, sigdi, tahmin = pencere.sigdir(req, 65536)
|
||||||
|
self.assertTrue(sigdi)
|
||||||
|
self.assertGreaterEqual(kirpilan, 1)
|
||||||
|
old = req["input"][0]["content"][0]["text"]
|
||||||
|
self.assertTrue(old.startswith("BAS ") and old.endswith(" SON"))
|
||||||
|
self.assertIn("çıkarıldı (köprü)", old)
|
||||||
|
self.assertEqual(req["input"][2]["content"][0]["text"], "yeni " * 3000) # the new message is intact
|
||||||
|
self.assertLessEqual(tahmin, int(65536 * pencere.DOLULUK))
|
||||||
|
|
||||||
|
def test_a_last_message_larger_than_the_window_is_reported_not_sent(self):
|
||||||
|
req = self.req("kisa", last="x" * 400000)
|
||||||
|
self.assertFalse(pencere.sigdir(req, 32768)[1])
|
||||||
|
|
||||||
|
|
||||||
|
class Phase(unittest.TestCase):
|
||||||
|
def run_stream(self, *events, chunk=37):
|
||||||
|
adapter = env["SseRewriter"](set())
|
||||||
|
raw = sse(*events)
|
||||||
|
out = b"".join(adapter.feed(raw[i:i + chunk]) for i in range(0, len(raw), chunk)) + adapter.flush()
|
||||||
|
return parse(out)
|
||||||
|
|
||||||
|
def test_message_before_a_tool_call_is_commentary(self):
|
||||||
|
call = {"type": "function_call", "id": "fc_1", "call_id": "call_1", "name": "shell", "arguments": "{}"}
|
||||||
|
out = self.run_stream(
|
||||||
|
{"type": "response.output_item.added", "item": message("")},
|
||||||
|
{"type": "response.output_text.delta", "item_id": "msg_1", "delta": "bakiyorum"},
|
||||||
|
{"type": "response.output_item.done", "item": message("bakiyorum")},
|
||||||
|
{"type": "response.output_item.added", "item": call},
|
||||||
|
{"type": "response.output_item.done", "item": call},
|
||||||
|
{"type": "response.completed", "response": {"output": [message("bakiyorum"), call]}})
|
||||||
|
self.assertEqual([e["type"] for e in out], [
|
||||||
|
"response.output_item.added", "response.output_text.delta", "response.output_item.done",
|
||||||
|
"response.output_item.added", "response.output_item.done", "response.completed"]) # order kept
|
||||||
|
self.assertEqual(out[2]["item"]["phase"], "commentary")
|
||||||
|
self.assertEqual(out[5]["response"]["output"][0]["phase"], "commentary")
|
||||||
|
self.assertNotIn("phase", out[5]["response"]["output"][1])
|
||||||
|
|
||||||
|
def test_last_message_without_tool_calls_is_the_final_answer(self):
|
||||||
|
out = self.run_stream(
|
||||||
|
{"type": "response.output_item.done", "item": message("ara", "msg_a")},
|
||||||
|
{"type": "response.output_item.added", "item": message("", "msg_b")},
|
||||||
|
{"type": "response.output_item.done", "item": message("son", "msg_b")},
|
||||||
|
{"type": "response.completed", "response": {"output": [message("ara", "msg_a"), message("son", "msg_b")]}})
|
||||||
|
done = [e for e in out if e["type"] == "response.output_item.done"]
|
||||||
|
self.assertEqual([e["item"]["phase"] for e in done], ["commentary", "final_answer"])
|
||||||
|
self.assertEqual([o["phase"] for o in out[-1]["response"]["output"]], ["commentary", "final_answer"])
|
||||||
|
self.assertEqual(out[-1]["type"], "response.completed") # done comes first
|
||||||
|
|
||||||
|
def test_existing_phase_and_cut_streams_are_left_alone(self):
|
||||||
|
native = dict(message("x"), phase="final_answer")
|
||||||
|
out = self.run_stream({"type": "response.output_item.done", "item": native},
|
||||||
|
{"type": "response.completed", "response": {"output": [dict(native)]}})
|
||||||
|
self.assertEqual(out[0]["item"]["phase"], "final_answer")
|
||||||
|
# No terminal event: the held message is still delivered, unmarked rather than guessed.
|
||||||
|
cut = self.run_stream({"type": "response.output_item.done", "item": message("yarim")})
|
||||||
|
self.assertEqual(len(cut), 1)
|
||||||
|
self.assertNotIn("phase", cut[0]["item"])
|
||||||
|
|
||||||
|
def test_overflow_failure_gets_the_code_codex_understands(self):
|
||||||
|
seen = []
|
||||||
|
adapter = env["SseRewriter"](set(), hedef="model-x", tasma=lambda hedef, metin: seen.append((hedef, metin)))
|
||||||
|
failed = {"type": "response.failed", "response": {"error": {
|
||||||
|
"code": "server_error", "message": "maximum context length is 32768 tokens"}}}
|
||||||
|
out = parse(adapter.feed(sse(failed)) + adapter.flush())
|
||||||
|
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
|
||||||
|
self.assertEqual(seen[0][0], "model-x")
|
||||||
|
other = {"type": "response.failed", "response": {"error": {"code": "rate_limit", "message": "slow down"}}}
|
||||||
|
self.assertEqual(parse(adapter.feed(sse(other)))[0]["response"]["error"]["code"], "rate_limit")
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayErrors(unittest.TestCase):
|
||||||
|
def test_permanent_gateway_error_becomes_a_failure_codex_shows_at_once(self):
|
||||||
|
adapter = env["SseRewriter"](set())
|
||||||
|
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
|
||||||
|
out = parse(adapter.feed(raw) + adapter.flush())
|
||||||
|
self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
|
||||||
|
self.assertIn("kredi ya da kota", out[1]["delta"])
|
||||||
|
self.assertIn("upstream 402", out[1]["delta"])
|
||||||
|
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 400"}})
|
||||||
|
out = parse(env["SseRewriter"](set()).feed(raw))
|
||||||
|
self.assertIn("nedenini bildirmedi", out[1]["delta"])
|
||||||
|
self.assertEqual(out[-1]["type"], "response.completed")
|
||||||
|
# A transient error is left alone so that Codex retries it.
|
||||||
|
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
|
||||||
|
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
|
||||||
|
|
||||||
|
|
||||||
|
class Images(unittest.TestCase):
|
||||||
|
def req(self):
|
||||||
|
img = {"type": "input_image", "image_url": "data:image/png;base64," + "A" * 2000}
|
||||||
|
return {"input": [
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "bak"}, dict(img)]},
|
||||||
|
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "OKUDUM"}]},
|
||||||
|
{"type": "function_call_output", "call_id": "c", "output": [dict(img)]},
|
||||||
|
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "ne yaziyordu"}]}]}
|
||||||
|
|
||||||
|
def test_text_model_gets_notes_vision_model_keeps_user_images(self):
|
||||||
|
text_only = self.req()
|
||||||
|
self.assertEqual(resim_context.resimleri_sadelestir(text_only)[0], 2)
|
||||||
|
self.assertNotIn("input_image", json.dumps(text_only))
|
||||||
|
vision = self.req()
|
||||||
|
self.assertEqual(resim_context.resimleri_sadelestir(vision, gorsel=True)[0], 1) # only the tool output
|
||||||
|
self.assertEqual(vision["input"][0]["content"][1]["type"], "input_image")
|
||||||
|
self.assertNotIn("input_image", json.dumps(vision["input"][2]))
|
||||||
|
big = self.req()
|
||||||
|
self.assertEqual(resim_context.resimleri_sadelestir(big, tut_bayt=500, gorsel=True)[0], 2) # over budget
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ClaudeOrani(unittest.TestCase):
|
||||||
|
def test_claude_targets_are_estimated_more_strictly_than_others(self):
|
||||||
|
import pencere as p
|
||||||
|
req = {"input": [{"type": "function_call_output", "call_id": "c", "output": "x" * 600_000},
|
||||||
|
{"type": "message", "role": "user", "content": "son"}]}
|
||||||
|
self.assertEqual(p.oran_sec("9router-cc-claude-opus-5-5"), p.CLAUDE_BAYT_BOLU_TOKEN)
|
||||||
|
self.assertEqual(p.oran_sec("9router-ag-gemini-3.8-flash-low"), p.BAYT_BOLU_TOKEN)
|
||||||
|
self.assertEqual(p.sigdir({"input": [dict(i) for i in req["input"]]}, 200_000)[0], 0) # 187500: fits
|
||||||
|
trimmed, fits, estimate = p.sigdir(req, 200_000, p.oran_sec("cc-claude-opus")) # 230769: trimmed
|
||||||
|
self.assertEqual((trimmed, fits), (1, True))
|
||||||
|
self.assertLess(estimate, 194_000)
|
||||||
|
self.assertTrue(p.kapali_hata_mi("upstream 400"))
|
||||||
|
# A per-minute rate limit is neither an overflow nor a window size to learn.
|
||||||
|
rate = "Rate limit reached: limit of 32768 tokens per minute, context window ok"
|
||||||
|
self.assertFalse(p.tasma_mi(rate))
|
||||||
|
self.assertIsNone(p.sinir_oku(rate))
|
||||||
|
self.assertTrue(p.tasma_mi("maximum context length is 65536 tokens"))
|
||||||
|
self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema"))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""An answer cut by the client is carried to the next turn of the same conversation, once."""
|
||||||
|
import json
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import yarim
|
||||||
|
|
||||||
|
|
||||||
|
def delta(text):
|
||||||
|
return (b"event: response.output_text.delta\ndata: "
|
||||||
|
+ json.dumps({"type": "response.output_text.delta", "delta": text}).encode() + b"\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def user(text):
|
||||||
|
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
|
||||||
|
|
||||||
|
|
||||||
|
class Yarim(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
yarim._KAYIT.clear()
|
||||||
|
|
||||||
|
def cut(self, key="sohbet-1", text="1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"):
|
||||||
|
izle = yarim.Izleyici(key, "ide/x")
|
||||||
|
raw = delta(text[:20]) + delta(text[20:])
|
||||||
|
for i in range(0, len(raw), 17):
|
||||||
|
izle.feed(raw[i:i + 17])
|
||||||
|
return izle
|
||||||
|
|
||||||
|
def test_cut_answer_reaches_the_next_user_message_once(self):
|
||||||
|
izle = self.cut()
|
||||||
|
self.assertTrue(yarim.kaydet(izle, simdi=100))
|
||||||
|
req = {"input": [user("liste yaz"), user("kaldigin yerden devam et")]}
|
||||||
|
self.assertGreater(yarim.uygula(req, "sohbet-1", simdi=200), 0)
|
||||||
|
note = req["input"][-1]["content"][-1]["text"]
|
||||||
|
self.assertIn("3. uc", note)
|
||||||
|
self.assertIn('model="ide/x"', note)
|
||||||
|
self.assertEqual(req["input"][-1]["content"][0]["text"], "kaldigin yerden devam et") # the user's words stay first
|
||||||
|
again = {"input": [user("bir daha")]}
|
||||||
|
self.assertEqual(yarim.uygula(again, "sohbet-1", simdi=300), 0) # used once
|
||||||
|
self.assertEqual(yarim.uygula({"input": [user("x")]}, "baska-sohbet", simdi=300), 0)
|
||||||
|
|
||||||
|
def test_finished_short_or_keyless_answers_are_not_kept(self):
|
||||||
|
done = self.cut()
|
||||||
|
done.feed(b'event: response.completed\ndata: {"type": "response.completed"}\n\n')
|
||||||
|
self.assertFalse(yarim.kaydet(done))
|
||||||
|
self.assertFalse(yarim.kaydet(self.cut(text="kisa")))
|
||||||
|
self.assertFalse(yarim.kaydet(self.cut(key="")))
|
||||||
|
|
||||||
|
def test_note_waits_for_a_real_user_turn_and_skips_text_codex_already_saved(self):
|
||||||
|
yarim.kaydet(self.cut(), simdi=100)
|
||||||
|
tool_turn = {"input": [user("x"), {"type": "function_call_output", "call_id": "c", "output": "ok"}]}
|
||||||
|
self.assertEqual(yarim.uygula(tool_turn, "sohbet-1", simdi=110), 0)
|
||||||
|
compaction = {"input": [user("x"), {"type": "compaction_trigger"}]}
|
||||||
|
self.assertEqual(yarim.uygula(compaction, "sohbet-1", simdi=110), 0)
|
||||||
|
self.assertIn("sohbet-1", yarim._KAYIT) # still waiting
|
||||||
|
saved = {"input": [{"type": "message", "role": "assistant", "content": [
|
||||||
|
{"type": "output_text", "text": "1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"}]}, user("devam")]}
|
||||||
|
self.assertEqual(yarim.uygula(saved, "sohbet-1", simdi=120), 0)
|
||||||
|
self.assertNotIn("sohbet-1", yarim._KAYIT)
|
||||||
|
yarim.kaydet(self.cut(), simdi=100)
|
||||||
|
self.assertEqual(yarim.uygula({"input": [user("gec")]}, "sohbet-1", simdi=100 + yarim.OMUR_SN + 1), 0) # expired
|
||||||
|
|
||||||
|
def test_long_text_keeps_head_and_tail(self):
|
||||||
|
yarim.kaydet(self.cut(text="BAS" + "x" * 40000 + "SON"), simdi=1)
|
||||||
|
req = {"input": [user("devam")]}
|
||||||
|
yarim.uygula(req, "sohbet-1", simdi=2)
|
||||||
|
note = req["input"][-1]["content"][-1]["text"]
|
||||||
|
self.assertLess(len(note), yarim.EN_COK_KARAKTER + 600)
|
||||||
|
self.assertIn("BAS", note)
|
||||||
|
self.assertTrue(note.rstrip().endswith("</yarim_cevap>"))
|
||||||
|
self.assertIn("SON\n</yarim_cevap>", note)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""Yarida kesilen cevabi sonraki modele tasir (10 Ekim 2026).
|
||||||
|
|
||||||
|
Olcum: Codex kapanir ya da kullanici cevabi durdurursa o ana kadar yazilan metin sohbet gecmisine girmiyor;
|
||||||
|
sonraki tur (ayni ya da baska modelle) "onceki cevabi goremiyorum" diyor. Kopru akisi zaten goruyor:
|
||||||
|
ISTEMCI baglantiyi kapattiginda (yanit bitmeden) o ana kadarki metni bellekte tutar ve ayni sohbetin
|
||||||
|
sonraki isteginde, kullanicinin yeni mesajina kisa bir not olarak ekler. Bir kez kullanilir.
|
||||||
|
|
||||||
|
Bilerek yapilmayanlar:
|
||||||
|
- Ust kaynak kopmasinda (istemci hala bagli) kayit yok: Codex o istegi kendisi yeniden gonderir; not eklenirse
|
||||||
|
model bastan yazmak yerine ortadan devam eder ve cevabin basi kaybolur.
|
||||||
|
- Yarim metin asistan mesaji olarak eklenmez: Claude "asistan mesajiyla biten istek"i reddeder ve abonelik
|
||||||
|
yolunda kimliksiz asistan ogesi risklidir. Kullanici mesajina eklenen not her saglayicida gecerlidir.
|
||||||
|
- Diske yazilmaz; kopru yeniden baslarsa kayit gider.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
OMUR_SN = 6 * 3600
|
||||||
|
EN_COK_KAYIT = 50
|
||||||
|
EN_COK_KARAKTER = 12_000 # nota konan yarim metnin ust siniri (basi ve sonu korunur)
|
||||||
|
EN_AZ_KARAKTER = 40 # bundan kisa yarim metin tasinmaya degmez
|
||||||
|
NOT_BASI = "[Köprü notu: bir önceki cevabın yarıda kesildi ve sohbet geçmişine kaydedilmedi."
|
||||||
|
_KILIT = threading.Lock()
|
||||||
|
_KAYIT = {} # sohbet anahtari -> {"zaman", "metin", "model"}
|
||||||
|
|
||||||
|
|
||||||
|
class Izleyici:
|
||||||
|
"""Akistan gecen parcalardan asistan metnini toplar; yanitin bitip bitmedigini izler."""
|
||||||
|
|
||||||
|
def __init__(self, anahtar, model):
|
||||||
|
self.anahtar, self.model = anahtar, model
|
||||||
|
self.buf = b""
|
||||||
|
self.parcalar = []
|
||||||
|
self.bitti = False
|
||||||
|
|
||||||
|
def feed(self, chunk):
|
||||||
|
self.buf += chunk
|
||||||
|
if b"\r" in self.buf:
|
||||||
|
self.buf = self.buf.replace(b"\r\n", b"\n") # CRLF ile ayrilan SSE de izlenir
|
||||||
|
while b"\n\n" in self.buf:
|
||||||
|
blok, self.buf = self.buf.split(b"\n\n", 1)
|
||||||
|
if b"response.output_text.delta" in blok:
|
||||||
|
for satir in blok.split(b"\n"):
|
||||||
|
if satir.startswith(b"data:"):
|
||||||
|
try:
|
||||||
|
d = json.loads(satir[5:])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
|
||||||
|
self.parcalar.append(d["delta"])
|
||||||
|
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
|
||||||
|
# Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
|
||||||
|
for satir in blok.split(b"\n"):
|
||||||
|
if satir.startswith(b"data:"):
|
||||||
|
try:
|
||||||
|
tur = json.loads(satir[5:]).get("type")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
continue
|
||||||
|
if tur in ("response.completed", "response.failed", "response.incomplete"):
|
||||||
|
self.bitti = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metin(self):
|
||||||
|
return "".join(self.parcalar)
|
||||||
|
|
||||||
|
|
||||||
|
def kaydet(izleyici, simdi=None):
|
||||||
|
"""Istemci kapatti ve yanit bitmemisti: yarim metni sakla. True: saklandi."""
|
||||||
|
metin = izleyici.metin.strip()
|
||||||
|
if not izleyici.anahtar or izleyici.bitti or len(metin) < EN_AZ_KARAKTER:
|
||||||
|
return False
|
||||||
|
simdi = time.time() if simdi is None else simdi
|
||||||
|
with _KILIT:
|
||||||
|
_KAYIT[izleyici.anahtar] = {"zaman": simdi, "metin": metin, "model": izleyici.model}
|
||||||
|
for eski in sorted(_KAYIT, key=lambda k: _KAYIT[k]["zaman"])[:max(0, len(_KAYIT) - EN_COK_KAYIT)]:
|
||||||
|
del _KAYIT[eski]
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _kisalt(metin):
|
||||||
|
if len(metin) <= EN_COK_KARAKTER:
|
||||||
|
return metin
|
||||||
|
bas, son = EN_COK_KARAKTER // 4, EN_COK_KARAKTER - EN_COK_KARAKTER // 4
|
||||||
|
return metin[:bas] + f"\n[... ortadan {len(metin) - bas - son} karakter çıkarıldı ...]\n" + metin[-son:]
|
||||||
|
|
||||||
|
|
||||||
|
def uygula(req, anahtar, simdi=None):
|
||||||
|
"""Ayni sohbetin sonraki isteginde yarim metni kullanicinin son mesajina not olarak ekler.
|
||||||
|
Donus: eklenen karakter sayisi (0: eklenmedi). Kayit yalniz basariyla eklenince ya da eskiyince silinir."""
|
||||||
|
if not anahtar:
|
||||||
|
return 0
|
||||||
|
simdi = time.time() if simdi is None else simdi
|
||||||
|
with _KILIT:
|
||||||
|
kayit = _KAYIT.get(anahtar)
|
||||||
|
if kayit and simdi - kayit["zaman"] > OMUR_SN:
|
||||||
|
del _KAYIT[anahtar]
|
||||||
|
kayit = None
|
||||||
|
girdi = req.get("input")
|
||||||
|
if not kayit or not isinstance(girdi, list) or not girdi:
|
||||||
|
return 0
|
||||||
|
son = None
|
||||||
|
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
|
||||||
|
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
|
||||||
|
continue
|
||||||
|
son = oge
|
||||||
|
break
|
||||||
|
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
|
||||||
|
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
|
||||||
|
metin = kayit["metin"]
|
||||||
|
ham = json.dumps(girdi, ensure_ascii=False)
|
||||||
|
if json.dumps(metin[:200], ensure_ascii=False)[1:-1] in ham:
|
||||||
|
with _KILIT: # Codex bu metni gecmise kendisi yazmis: ikinci kez eklenmez
|
||||||
|
_KAYIT.pop(anahtar, None)
|
||||||
|
return 0
|
||||||
|
notu = (f"{NOT_BASI} Kesilmeden önce yazdığın metin aşağıda. Kullanıcı devam etmeni isterse baştan yazma, "
|
||||||
|
f"kaldığın yerden sürdür.]\n<yarim_cevap model=\"{kayit['model']}\">\n{_kisalt(metin)}\n</yarim_cevap>")
|
||||||
|
icerik = son.get("content")
|
||||||
|
if isinstance(icerik, str):
|
||||||
|
son["content"] = [{"type": "input_text", "text": icerik}, {"type": "input_text", "text": notu}]
|
||||||
|
elif isinstance(icerik, list):
|
||||||
|
icerik.append({"type": "input_text", "text": notu})
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
with _KILIT:
|
||||||
|
_KAYIT.pop(anahtar, None)
|
||||||
|
return len(notu)
|
||||||
@@ -230,7 +230,7 @@ describe('bridge configuration and command', () => {
|
|||||||
routes: {},
|
routes: {},
|
||||||
nine_router_catalog: { enabled: false },
|
nine_router_catalog: { enabled: false },
|
||||||
pano_kanca_user: false,
|
pano_kanca_user: false,
|
||||||
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
|
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
|
||||||
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
|
// 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.
|
// another version is restarted so an updated IDE never keeps an old bridge alive.
|
||||||
export const CODEX_BRIDGE_VERSION = '5'
|
export const CODEX_BRIDGE_VERSION = '8'
|
||||||
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
|
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'
|
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
|
||||||
@@ -339,7 +339,7 @@ export function codexBridgeConfig() {
|
|||||||
routes: {},
|
routes: {},
|
||||||
nine_router_catalog: { enabled: false },
|
nine_router_catalog: { enabled: false },
|
||||||
pano_kanca_user: false,
|
pano_kanca_user: false,
|
||||||
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
|
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export interface CodexLinkDeps {
|
|||||||
|
|
||||||
// The single wait this feature owns: how long a freshly spawned bridge gets to
|
// The single wait this feature owns: how long a freshly spawned bridge gets to
|
||||||
// answer its status endpoint before the apply gives up.
|
// answer its status endpoint before the apply gives up.
|
||||||
export const CODEX_BRIDGE_START_BUDGET_MS = 10_000
|
export const CODEX_BRIDGE_START_BUDGET_MS = 45_000
|
||||||
const START_POLL_INTERVAL_MS = 500
|
const START_POLL_INTERVAL_MS = 500
|
||||||
// A probe of an already-running (or absent) local bridge. Loopback answers or
|
// A probe of an already-running (or absent) local bridge. Loopback answers or
|
||||||
// refuses at once; the bound only keeps Settings from hanging on a wedged port.
|
// refuses at once; the bound only keeps Settings from hanging on a wedged port.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "aiturk-ide",
|
"name": "aiturk-ide",
|
||||||
"productName": "AITURK IDE",
|
"productName": "AITURK IDE",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0-beta.24",
|
"version": "1.0.0-beta.27",
|
||||||
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
|
||||||
"author": "AITURK / TurkServis",
|
"author": "AITURK / TurkServis",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
Generated
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
"apps/desktop": {
|
"apps/desktop": {
|
||||||
"name": "aiturk-ide",
|
"name": "aiturk-ide",
|
||||||
"version": "1.0.0-beta.24",
|
"version": "1.0.0-beta.27",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@assistant-ui/core": "0.2.23",
|
"@assistant-ui/core": "0.2.23",
|
||||||
|
|||||||
Reference in New Issue
Block a user