Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48103b9ad3 | ||
|
|
615fedde01 | ||
|
|
a46ff51019 | ||
|
|
8a612beba7 | ||
|
|
7c0dc467d5 | ||
|
|
e2793fa38f | ||
|
|
1e2b8e2b9c | ||
|
|
1445e204da | ||
|
|
d9a7661281 |
@@ -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.
|
||||||
@@ -45,6 +45,29 @@ for the chat, i.e. until the user picks another one in the list.
|
|||||||
|
|
||||||
Setting: `sesle_model` (default on).
|
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
|
## 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).
|
Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn).
|
||||||
@@ -65,4 +88,4 @@ Not changed: subscription models, their order and names, and everything on the v
|
|||||||
|
|
||||||
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
|
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> hazır." 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.
|
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.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Data directory (bridge.json, log/): --home <dir> or AITURK_CODEX_BRIDGE_HOME,
|
|||||||
otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME.
|
otherwise the script's own directory. Agent home: --hermes-home or HERMES_HOME.
|
||||||
Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop.
|
Local control: GET /aiturk/codex-baglama/status, POST /aiturk/codex-baglama/stop.
|
||||||
"""
|
"""
|
||||||
import http.server, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
|
import http.server, http.client, urllib.request, urllib.error, json, os, sys, time, threading, re, copy, hashlib
|
||||||
from gemini_context import coalesce_context
|
from gemini_context import coalesce_context
|
||||||
try:
|
try:
|
||||||
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
|
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
|
||||||
@@ -28,9 +28,15 @@ import ozet_kopru
|
|||||||
import pencere
|
import pencere
|
||||||
import yarim
|
import yarim
|
||||||
import model_gecis
|
import model_gecis
|
||||||
|
import is_hatti
|
||||||
|
|
||||||
SURUM = "8"
|
SURUM = "8"
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
try:
|
||||||
|
with open(os.path.abspath(__file__), "rb") as _f:
|
||||||
|
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] # ayni surum numarasi altindaki farkli kodu ayirt eder
|
||||||
|
except OSError:
|
||||||
|
KOD = ""
|
||||||
|
|
||||||
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
|
# Codex sends request bodies zstd-compressed. Python gained zstd in 3.14; for older
|
||||||
# interpreters the matching `zstandard` build ships next to this file.
|
# interpreters the matching `zstandard` build ships next to this file.
|
||||||
@@ -96,8 +102,13 @@ def is_timeout(error):
|
|||||||
"""True for a socket/read timeout, also when urllib wraps it in URLError."""
|
"""True for a socket/read timeout, also when urllib wraps it in URLError."""
|
||||||
reason = getattr(error, "reason", None)
|
reason = getattr(error, "reason", None)
|
||||||
return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError)
|
return isinstance(error, TimeoutError) or isinstance(reason, TimeoutError)
|
||||||
|
def kopma_mi(error):
|
||||||
|
"""Baglanti kurulduktan sonra uzak uc kapatti ya da sifirladi (yanit basliklari gelmeden): istek ulasmis ve
|
||||||
|
is baslamis olabilir. Baglanti hic kurulamadiysa (reddedildi, DNS) False."""
|
||||||
|
reason = getattr(error, "reason", None)
|
||||||
|
turler = (ConnectionResetError, ConnectionAbortedError, http.client.IncompleteRead) # RemoteDisconnected dahil
|
||||||
|
return isinstance(error, turler) or isinstance(reason, turler)
|
||||||
DEBUG = bool(CFG.get("debug", False))
|
DEBUG = bool(CFG.get("debug", False))
|
||||||
DISPLAY_MAP = CFG.get("display_map", {})
|
|
||||||
UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
|
UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
|
||||||
# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir.
|
# Bu metinleri iceren hatalar kalicidir; yeniden denemek yerine hemen Codex'e iletilir.
|
||||||
NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"])
|
NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models failed", "model not found", "does not exist", "insufficient_balance", "allowance exhausted"])
|
||||||
@@ -107,7 +118,7 @@ lock = threading.Lock()
|
|||||||
seq = [0]
|
seq = [0]
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
line = time.strftime("%H:%M:%S ") + msg
|
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
|
||||||
print(line, flush=True)
|
print(line, flush=True)
|
||||||
with lock:
|
with lock:
|
||||||
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
|
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
|
||||||
@@ -201,7 +212,7 @@ def load_ide_catalog(ag=True):
|
|||||||
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels,
|
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels,
|
||||||
"kind": g["kind"], "context_window": g.get("pencere")})
|
"kind": g["kind"], "context_window": g.get("pencere")})
|
||||||
IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"),
|
IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"),
|
||||||
"kaynak": g.get("pencere_kaynak") or ""}
|
"kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))}
|
||||||
model_map[slug] = (IDE_ROUTE, g["id"], None)
|
model_map[slug] = (IDE_ROUTE, g["id"], None)
|
||||||
n += 1
|
n += 1
|
||||||
else:
|
else:
|
||||||
@@ -235,6 +246,23 @@ def catalog_refresh_loop():
|
|||||||
|
|
||||||
threading.Thread(target=catalog_refresh_loop, daemon=True).start()
|
threading.Thread(target=catalog_refresh_loop, daemon=True).start()
|
||||||
|
|
||||||
|
# ---- AITURK is hatti: isi gonderebilen AITURK komutu (is_hatti.py). Kurulum sonradan gelebilir: yoksa 2 dk'da bir bakilir.
|
||||||
|
HAT_KOMUT = [None]
|
||||||
|
|
||||||
|
def hat_komut_loop():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if (CFG.get("aiturk_hat") or {}).get("enabled") is not False:
|
||||||
|
bulunan = is_hatti.komut_bul()
|
||||||
|
if bulunan != HAT_KOMUT[0]:
|
||||||
|
HAT_KOMUT[0] = bulunan
|
||||||
|
log(f"AITURK is hatti: {'acik, komut ' + bulunan if bulunan else 'kapali (aiturk is komutu yok)'}")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"AITURK is hatti komutu aranamadi: {type(e).__name__}: {e}")
|
||||||
|
time.sleep(600 if HAT_KOMUT[0] else 120)
|
||||||
|
|
||||||
|
threading.Thread(target=hat_komut_loop, daemon=True).start()
|
||||||
|
|
||||||
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
|
LAST_MODELS = {} # hesap kimligi -> son basarili model listesi (chatgpt.com hata verirse bu verilir)
|
||||||
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
|
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
|
||||||
|
|
||||||
@@ -250,7 +278,7 @@ def save_last_models(acct, hdrs, body):
|
|||||||
yol = _cache_path(acct)
|
yol = _cache_path(acct)
|
||||||
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"body": body.decode("utf-8")}
|
"body": body.decode("utf-8")}
|
||||||
gecici = yol + ".tmp"
|
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # ayni hesabin iki es zamanli kaydi carpismasin
|
||||||
with open(gecici, "w", encoding="utf-8") as f:
|
with open(gecici, "w", encoding="utf-8") as f:
|
||||||
json.dump(veri, f, ensure_ascii=False)
|
json.dump(veri, f, ensure_ascii=False)
|
||||||
os.replace(gecici, yol) # yarim dosya birakmamak icin
|
os.replace(gecici, yol) # yarim dosya birakmamak icin
|
||||||
@@ -280,6 +308,48 @@ SLIM_CLONES = bool(CFG.get("slim_clones", False))
|
|||||||
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
|
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
|
||||||
|
|
||||||
# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ----
|
# ---- Codex'in yeni arac bicimini duz 'tools' listesine acma (9Router gibi ceviricilerin anlamasi icin) ----
|
||||||
|
def sema_duzelt(sema):
|
||||||
|
"""Arac parametre semasinin EN UST duzeyindeki oneOf/anyOf/allOf'u tek nesne semasina indirir.
|
||||||
|
Claude bunu reddediyor ("input_schema does not support oneOf, allOf, or anyOf at the top level"; olcum
|
||||||
|
11 Ekim 2026: masaustu Codex'in klasik arac listesindeki bir arac yuzunden her Opus istegi 400 aldi).
|
||||||
|
Secenek nesnelerinin alanlari birlestirilir; zorunlu alanlar oneOf/anyOf'ta kesisim, allOf'ta birlesimdir.
|
||||||
|
Donus: (sema, degisti mi)."""
|
||||||
|
if not isinstance(sema, dict) or not any(k in sema for k in ("oneOf", "anyOf", "allOf")):
|
||||||
|
return sema, False
|
||||||
|
yeni = {k: v for k, v in sema.items() if k not in ("oneOf", "anyOf", "allOf")}
|
||||||
|
alanlar = dict(yeni.get("properties") or {})
|
||||||
|
zorunlu = set(yeni.get("required") or [])
|
||||||
|
for anahtar in ("oneOf", "anyOf", "allOf"):
|
||||||
|
secenekler = [x for x in (sema.get(anahtar) or []) if isinstance(x, dict)]
|
||||||
|
kesisim = None
|
||||||
|
for secenek in secenekler:
|
||||||
|
secenek, _ = sema_duzelt(secenek)
|
||||||
|
for ad, tanim in (secenek.get("properties") or {}).items():
|
||||||
|
alanlar.setdefault(ad, tanim)
|
||||||
|
gerekli = set(secenek.get("required") or [])
|
||||||
|
if anahtar == "allOf":
|
||||||
|
zorunlu |= gerekli
|
||||||
|
else:
|
||||||
|
kesisim = gerekli if kesisim is None else (kesisim & gerekli)
|
||||||
|
if anahtar != "allOf" and kesisim:
|
||||||
|
zorunlu |= kesisim
|
||||||
|
yeni["type"] = "object"
|
||||||
|
yeni["properties"] = alanlar
|
||||||
|
if zorunlu:
|
||||||
|
yeni["required"] = sorted(a for a in zorunlu if a in alanlar)
|
||||||
|
else:
|
||||||
|
yeni.pop("required", None)
|
||||||
|
return yeni, True
|
||||||
|
|
||||||
|
def arac_semalarini_duzelt(req):
|
||||||
|
"""Dis modele giden duz arac listesinde ust duzey birlesimli semalari duzeltir. Donus: duzeltilen arac sayisi."""
|
||||||
|
n = 0
|
||||||
|
for t in req.get("tools") or []:
|
||||||
|
if isinstance(t, dict) and isinstance(t.get("parameters"), dict):
|
||||||
|
t["parameters"], degisti = sema_duzelt(t["parameters"])
|
||||||
|
n += bool(degisti)
|
||||||
|
return n
|
||||||
|
|
||||||
def flatten_request(req):
|
def flatten_request(req):
|
||||||
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
|
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
|
||||||
custom = set()
|
custom = set()
|
||||||
@@ -444,7 +514,8 @@ class SseRewriter:
|
|||||||
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
|
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
|
||||||
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
|
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
|
||||||
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
|
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
|
||||||
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None):
|
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False):
|
||||||
|
self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor
|
||||||
self.custom = custom
|
self.custom = custom
|
||||||
self.buf = b""
|
self.buf = b""
|
||||||
self.custom_ids = set()
|
self.custom_ids = set()
|
||||||
@@ -458,6 +529,25 @@ class SseRewriter:
|
|||||||
if d is not None and not d["item"].get("phase"):
|
if d is not None and not d["item"].get("phase"):
|
||||||
d["item"]["phase"] = phase
|
d["item"]["phase"] = phase
|
||||||
return d
|
return d
|
||||||
|
def _gorunur_hata(self, mesaj):
|
||||||
|
"""Kalici hata: Codex'in sessizce yeniden deneyip turu cevapsiz bitirmesi yerine (denetim 11 Ekim 2026)
|
||||||
|
hata, gorunur bir asistan mesaji olarak yazilir ve yanit normal bicimde tamamlanir. Sesli kullanimda
|
||||||
|
kullanici hatayi duyar."""
|
||||||
|
damga = "%d" % int(time.time() * 1000)
|
||||||
|
rid, mid = "resp_astrabridge_hata_" + damga, "msg_astrabridge_hata_" + damga
|
||||||
|
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
|
||||||
|
"content": [{"type": "output_text", "text": mesaj, "annotations": []}]}
|
||||||
|
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
|
||||||
|
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
|
||||||
|
return [
|
||||||
|
{"type": "response.output_item.added", "output_index": 0,
|
||||||
|
"item": {"id": mid, "type": "message", "role": "assistant", "status": "in_progress", "content": []}},
|
||||||
|
{"type": "response.output_text.delta", "item_id": mid, "output_index": 0, "content_index": 0, "delta": mesaj},
|
||||||
|
{"type": "response.output_item.done", "output_index": 0, "item": oge},
|
||||||
|
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
|
||||||
|
"output": [oge], "usage": kullanim}},
|
||||||
|
]
|
||||||
|
|
||||||
def _sirala(self, d):
|
def _sirala(self, d):
|
||||||
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
|
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
|
||||||
t = d.get("type", "")
|
t = d.get("type", "")
|
||||||
@@ -466,29 +556,47 @@ class SseRewriter:
|
|||||||
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
|
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
|
||||||
"status_code": d.get("status_code")}}
|
"status_code": d.get("status_code")}}
|
||||||
if t == "error" and isinstance(d.get("error"), dict):
|
if t == "error" and isinstance(d.get("error"), dict):
|
||||||
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
|
cevrilen = self._hata_cevir(d)
|
||||||
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
|
if cevrilen is not None:
|
||||||
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
|
# Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir.
|
||||||
metin = str(d["error"].get("message") or d["error"].get("type") or "")
|
once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else []
|
||||||
if pencere.aracsiz_mi(metin):
|
return once + cevrilen
|
||||||
if self.aracsiz is not None:
|
return self._sirala_normal(d, t)
|
||||||
self.aracsiz(self.hedef)
|
|
||||||
return [{"type": "response.failed", "response": {
|
def _hata_cevir(self, d):
|
||||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
"""Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir."""
|
||||||
"error": {"code": "invalid_request_error", "message":
|
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
|
||||||
"Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin."}}}]
|
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
|
||||||
if pencere.tasma_mi(metin):
|
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
|
||||||
if self.tasma is not None:
|
metin = str(d["error"].get("message") or d["error"].get("type") or "")
|
||||||
self.tasma(self.hedef, metin)
|
if pencere.aracsiz_mi(metin):
|
||||||
return [{"type": "response.failed", "response": {
|
if self.aracsiz is not None:
|
||||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
self.aracsiz(self.hedef)
|
||||||
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
|
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
|
||||||
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
|
if self.dolu and pencere.kapali_hata_mi(metin):
|
||||||
kod = "insufficient_quota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "invalid_api_key"
|
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
|
||||||
d = {"type": "response.failed", "response": {
|
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
|
||||||
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
return [{"type": "response.failed", "response": {
|
||||||
"error": {"code": kod, "message": "AITURK ağ geçidi isteği reddetti: " + metin[:300]}}}
|
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||||||
t = "response.failed"
|
"error": {"code": "context_length_exceeded", "message":
|
||||||
|
"Geçit isteği reddetti ve nedenini bildirmedi. Sohbet seçili modelin bağlam penceresine çok yakın; "
|
||||||
|
"sığmamış olabilir. Sohbeti sıkıştırın ya da daha büyük pencereli bir model seçin."}}}]
|
||||||
|
if pencere.tasma_mi(metin):
|
||||||
|
if self.tasma is not None:
|
||||||
|
self.tasma(self.hedef, metin)
|
||||||
|
return [{"type": "response.failed", "response": {
|
||||||
|
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
|
||||||
|
"error": {"code": "context_length_exceeded", "message": metin[:300]}}}]
|
||||||
|
if re.search(r"(?<![0-9])(401|402|403)(?![0-9])|credit|quota|billing|payment|unauthori[sz]ed|forbidden", metin, re.I):
|
||||||
|
neden = "kredi ya da kota" if re.search(r"402|credit|quota|billing|payment", metin, re.I) else "yetki"
|
||||||
|
return self._gorunur_hata(f"Seçili model şu an kullanılamıyor ({neden} sorunu). Başka bir model seçin. "
|
||||||
|
f"Ayrıntı: {metin[:200]}")
|
||||||
|
if pencere.kapali_hata_mi(metin):
|
||||||
|
return self._gorunur_hata("Seçili model isteği reddetti ve nedenini bildirmedi. Başka bir model seçip "
|
||||||
|
"yeniden deneyin.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _sirala_normal(self, d, t):
|
||||||
item = d.get("item") if isinstance(d.get("item"), dict) else None
|
item = d.get("item") if isinstance(d.get("item"), dict) else None
|
||||||
if item and item.get("type") in ARAC_OGELERI:
|
if item and item.get("type") in ARAC_OGELERI:
|
||||||
self.arac_goruldu = True
|
self.arac_goruldu = True
|
||||||
@@ -545,6 +653,8 @@ class SseRewriter:
|
|||||||
return d
|
return d
|
||||||
def feed(self, chunk):
|
def feed(self, chunk):
|
||||||
self.buf += 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 cevrilir
|
||||||
out = b""
|
out = b""
|
||||||
while b"\n\n" in self.buf:
|
while b"\n\n" in self.buf:
|
||||||
block, self.buf = self.buf.split(b"\n\n", 1)
|
block, self.buf = self.buf.split(b"\n\n", 1)
|
||||||
@@ -556,6 +666,8 @@ class SseRewriter:
|
|||||||
d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx))
|
d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx))
|
||||||
except Exception:
|
except Exception:
|
||||||
out += block + b"\n\n"; continue
|
out += block + b"\n\n"; continue
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
out += block + b"\n\n"; continue # "data: null" gibi govde akisi kesmez, oldugu gibi gecer
|
||||||
d = self._fix(d)
|
d = self._fix(d)
|
||||||
if d is None:
|
if d is None:
|
||||||
continue
|
continue
|
||||||
@@ -658,6 +770,7 @@ def decompress(body, enc):
|
|||||||
|
|
||||||
OZET_FRENI = ozet_kopru.DonguFreni()
|
OZET_FRENI = ozet_kopru.DonguFreni()
|
||||||
YONLENDIRME = model_gecis.Yonlendirme(HOME)
|
YONLENDIRME = model_gecis.Yonlendirme(HOME)
|
||||||
|
GORULEN_ISTEMCI = set() # originator degerleri; her biri gunluge bir kez yazilir
|
||||||
_BILINEN = {"kaynak": None, "liste": []}
|
_BILINEN = {"kaynak": None, "liste": []}
|
||||||
|
|
||||||
def bilinen_modeller():
|
def bilinen_modeller():
|
||||||
@@ -677,6 +790,31 @@ def bilinen_modeller():
|
|||||||
if m.get("slug") and m["slug"] not in gorulen]
|
if m.get("slug") and m["slug"] not in gorulen]
|
||||||
return liste
|
return liste
|
||||||
|
|
||||||
|
_GECIT = {"zaman": 0.0, "liste": []}
|
||||||
|
|
||||||
|
def gecit_modelleri():
|
||||||
|
"""AITURK gecidindeki BUTUN modeller (Codex listesinde gorunmeyenler dahil), "ide/<kimlik>" olarak. Soylenen ad
|
||||||
|
Codex listesinde yoksa buraya bakilir (olcum 11 Ekim 2026: "Seed 2.1 Turbo" gecitte vardi, listede yoktu).
|
||||||
|
10 dakika bellekte tutulur; alinamazsa bos liste."""
|
||||||
|
simdi = time.time()
|
||||||
|
if simdi - _GECIT["zaman"] < 600:
|
||||||
|
return _GECIT["liste"]
|
||||||
|
_GECIT["zaman"] = simdi
|
||||||
|
rota = ROUTES.get("aiturk-ide")
|
||||||
|
if not rota:
|
||||||
|
_GECIT["liste"] = []
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
adres = rota["url"].rsplit("/", 1)[0] + "/models"
|
||||||
|
istek = urllib.request.Request(adres, headers={"Authorization": f"Bearer {rota['key']}", "User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(istek, timeout=6) as yanit:
|
||||||
|
veri = json.loads(yanit.read())
|
||||||
|
_GECIT["liste"] = [("ide/" + m["id"], m["id"]) for m in veri.get("data", []) if isinstance(m, dict) and m.get("id")]
|
||||||
|
except Exception as e:
|
||||||
|
log(f"gecit model listesi alinamadi: {type(e).__name__}: {e}")
|
||||||
|
_GECIT["liste"] = []
|
||||||
|
return _GECIT["liste"]
|
||||||
|
|
||||||
def model_adi(slug):
|
def model_adi(slug):
|
||||||
return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug
|
return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug
|
||||||
|
|
||||||
@@ -688,7 +826,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
|
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
|
||||||
out = bytearray()
|
out = bytearray()
|
||||||
while True:
|
while True:
|
||||||
line = self.rfile.readline().strip()
|
raw = self.rfile.readline()
|
||||||
|
if not raw:
|
||||||
|
break # baglanti kapandi (EOF): sonsuz donguye girme
|
||||||
|
line = raw.strip()
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
size = int(line.split(b";")[0], 16)
|
size = int(line.split(b";")[0], 16)
|
||||||
@@ -716,7 +857,15 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
|
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
chunk = resp.read(4096)
|
try:
|
||||||
|
# read1: gelen kadarini hemen verir; read(4096) kucuk parcalari 4096 bayt dolana kadar bekletiyordu.
|
||||||
|
chunk = resp.read1(4096)
|
||||||
|
except (OSError, http.client.HTTPException) as e:
|
||||||
|
# Ust kaynak koptu (istemci degil): yarim cevap kaydi yapilmaz, baglanti kapatilir ki Codex
|
||||||
|
# yarim akisi tam sanmasin ve yeniden denesin.
|
||||||
|
log(f"{tag} ust kaynak koptu ({total} bayt): {type(e).__name__}: {e}")
|
||||||
|
self.close_connection = True
|
||||||
|
return
|
||||||
if not chunk: break
|
if not chunk: break
|
||||||
total += len(chunk)
|
total += len(chunk)
|
||||||
if len(bas) < 600:
|
if len(bas) < 600:
|
||||||
@@ -779,9 +928,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
self.command = "GET"
|
self.command = "GET"
|
||||||
if self.command == "GET" and path.rstrip("/").endswith("/status"):
|
if self.command == "GET" and path.rstrip("/").endswith("/status"):
|
||||||
durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ")
|
durum, _, sayi = (IDE_DURUM[0] or "kapali, 0 model").partition(", ")
|
||||||
return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "zstd": ZSTD_OK, "ide_durum": durum,
|
return self._send_json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": durum,
|
||||||
"ide_model": int((sayi.split() or ["0"])[0]),
|
"ide_model": int((sayi.split() or ["0"])[0]),
|
||||||
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False))})
|
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
|
||||||
|
"is_hatti": bool(HAT_KOMUT[0])})
|
||||||
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
|
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
|
||||||
self._send_json(200, {"ok": True})
|
self._send_json(200, {"ok": True})
|
||||||
log("AITURK IDE istegiyle kapatiliyor")
|
log("AITURK IDE istegiyle kapatiliyor")
|
||||||
@@ -805,7 +955,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return self._control(path)
|
return self._control(path)
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
|
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
|
||||||
if self.command == "POST" and path.rstrip("/").endswith("/responses"):
|
if self.command == "POST" and path.split("?", 1)[0].rstrip("/").endswith("/responses"):
|
||||||
return self._responses(n, path, body)
|
return self._responses(n, path, body)
|
||||||
return self._passthrough(n, path, body)
|
return self._passthrough(n, path, body)
|
||||||
|
|
||||||
@@ -823,23 +973,19 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
resp = e; status = e.code
|
resp = e; status = e.code
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"{tag} HATA {e}"); return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
|
log(f"{tag} HATA {e}")
|
||||||
|
# Ag yokken de model secici bosalmasin: chatgpt.com'a hic ulasilamazsa son basarili liste verilir.
|
||||||
|
if self.command == "GET" and "/models" in path and self._son_listeyi_ver(tag, self.headers.get("chatgpt-account-id") or "-"):
|
||||||
|
return
|
||||||
|
return self._send_json(502, {"error": {"message": f"bridge passthrough: {e}"}})
|
||||||
if self.command == "GET" and "/models" in path:
|
if self.command == "GET" and "/models" in path:
|
||||||
acct = self.headers.get("chatgpt-account-id") or "-"
|
acct = self.headers.get("chatgpt-account-id") or "-"
|
||||||
tok = (self.headers.get("Authorization") or "")[-8:]
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
out = self._models(resp, tag)
|
out = self._models(resp, tag)
|
||||||
return out
|
return out
|
||||||
body = resp.read()[:300]
|
body = resp.read()[:300]
|
||||||
log(f"{tag} chatgpt.com {status} verdi (hesap={acct} token=..{tok}) govde={body[:160]!r}")
|
log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}")
|
||||||
cached = LAST_MODELS.get(acct)
|
if self._son_listeyi_ver(tag, acct):
|
||||||
if cached:
|
|
||||||
self.send_response(200)
|
|
||||||
for k, v in cached["headers"]:
|
|
||||||
self.send_header(k, v)
|
|
||||||
self.send_header("Content-Length", str(len(cached["body"])))
|
|
||||||
self.end_headers(); self.wfile.write(cached["body"])
|
|
||||||
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
|
|
||||||
return
|
return
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
|
||||||
@@ -864,6 +1010,19 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
|
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
|
||||||
self._stream(resp, status, tag, rewriter, izle)
|
self._stream(resp, status, tag, rewriter, izle)
|
||||||
|
|
||||||
|
def _son_listeyi_ver(self, tag, acct):
|
||||||
|
"""Bu hesabin son basarili model listesini gonderir. True: gonderildi."""
|
||||||
|
cached = LAST_MODELS.get(acct)
|
||||||
|
if not cached:
|
||||||
|
return False
|
||||||
|
self.send_response(200)
|
||||||
|
for k, v in cached["headers"]:
|
||||||
|
self.send_header(k, v)
|
||||||
|
self.send_header("Content-Length", str(len(cached["body"])))
|
||||||
|
self.end_headers(); self.wfile.write(cached["body"])
|
||||||
|
log(f"{tag} son basarili liste verildi ({len(cached['body'])} bayt, {cached['at']})")
|
||||||
|
return True
|
||||||
|
|
||||||
def _openai_ozet_coz(self, n, oge, ozet_cfg):
|
def _openai_ozet_coz(self, n, oge, ozet_cfg):
|
||||||
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
|
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
|
||||||
klasor = os.path.join(LOGDIR, "ozet-onbellek")
|
klasor = os.path.join(LOGDIR, "ozet-onbellek")
|
||||||
@@ -929,12 +1088,11 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
data = json.loads(raw)
|
data = json.loads(raw)
|
||||||
models = data.get("models", [])
|
models = data.get("models", [])
|
||||||
changed = 0
|
changed = 0
|
||||||
for m in models:
|
# Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz).
|
||||||
new = DISPLAY_MAP.get(m.get("slug"))
|
|
||||||
if new:
|
|
||||||
m["display_name"] = new; changed += 1
|
|
||||||
by_slug = {m.get("slug"): m for m in models}
|
by_slug = {m.get("slug"): m for m in models}
|
||||||
for x in EXTRA_MODELS:
|
ozgun_kimlikler = set(by_slug)
|
||||||
|
ekler = EXTRA_MODELS # tek kopya: liste hazirlanirken katalog yenilenirse sira ve icerik tutarli kalir
|
||||||
|
for x in ekler:
|
||||||
if x["slug"] in by_slug:
|
if x["slug"] in by_slug:
|
||||||
continue
|
continue
|
||||||
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
|
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
|
||||||
@@ -974,13 +1132,14 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if not m.get("base_instructions") and not has_tmpl:
|
if not m.get("base_instructions") and not has_tmpl:
|
||||||
m.update(saved)
|
m.update(saved)
|
||||||
models.append(m); changed += 1
|
models.append(m); changed += 1
|
||||||
if EXTRA_MODELS:
|
if ekler:
|
||||||
ide_catalog.sirala_codex(models, EXTRA_MODELS)
|
ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler)
|
||||||
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"{tag} model listesi degistirilemedi: {e}")
|
log(f"{tag} model listesi degistirilemedi: {e}")
|
||||||
out = raw; changed = 0
|
out = raw; changed = 0
|
||||||
raw_len = len(out)
|
raw_len = len(out)
|
||||||
|
duz = out # sikistirilmamis govde: yedege bu yazilir
|
||||||
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
|
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
|
||||||
enc_out = None
|
enc_out = None
|
||||||
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
|
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
|
||||||
@@ -993,15 +1152,16 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if kl in ("etag", "x-models-etag") and changed:
|
if kl in ("etag", "x-models-etag") and changed:
|
||||||
v = catalog_etag
|
v = catalog_etag
|
||||||
hdrs_out.append((k, v))
|
hdrs_out.append((k, v))
|
||||||
if enc_out:
|
if changed:
|
||||||
hdrs_out.append(("Content-Encoding", enc_out))
|
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
|
||||||
if changed and not enc_out:
|
|
||||||
acct_ = self.headers.get("chatgpt-account-id") or "-"
|
acct_ = self.headers.get("chatgpt-account-id") or "-"
|
||||||
onceki = LAST_MODELS.get(acct_)
|
onceki = LAST_MODELS.get(acct_)
|
||||||
LAST_MODELS[acct_] = {"headers": hdrs_out, "body": out, "at": time.strftime("%H:%M:%S")}
|
LAST_MODELS[acct_] = {"headers": list(hdrs_out), "body": duz, "at": time.strftime("%Y-%m-%d %H:%M:%S")}
|
||||||
# Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz.
|
# Liste 4 dakikada bir isteniyor; sadece icerik degistiyse diske yaz.
|
||||||
if not onceki or onceki.get("body") != out:
|
if not onceki or onceki.get("body") != duz:
|
||||||
save_last_models(acct_, hdrs_out, out)
|
save_last_models(acct_, list(hdrs_out), duz)
|
||||||
|
if enc_out:
|
||||||
|
hdrs_out.append(("Content-Encoding", enc_out))
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
for k, v in hdrs_out:
|
for k, v in hdrs_out:
|
||||||
self.send_header(k, v)
|
self.send_header(k, v)
|
||||||
@@ -1032,9 +1192,23 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return None
|
return None
|
||||||
log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}")
|
log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {etkin}")
|
||||||
return f"Sohbetin modeli şu an {model_adi(etkin)}."
|
return f"Sohbetin modeli şu an {model_adi(etkin)}."
|
||||||
|
if komut["tur"] == "nasil":
|
||||||
|
if ortada:
|
||||||
|
return None
|
||||||
|
log(f"[{n} {gelen}] adsiz model komutu: nasil soylenecegi anlatildi")
|
||||||
|
return f"Sohbetin modeli şu an {model_adi(etkin)}. " + model_gecis.NASIL
|
||||||
sonuc = model_gecis.eslestir(komut["ad"], bilinen_modeller())
|
sonuc = model_gecis.eslestir(komut["ad"], bilinen_modeller())
|
||||||
if not sonuc:
|
if not sonuc:
|
||||||
return None # ad hicbir modele benzemiyor: komut degil, modele gider
|
sonuc = model_gecis.eslestir(komut["ad"], gecit_modelleri()) # Codex listesinde olmayan gecit modeli
|
||||||
|
if not komut.get("kesin", True) and not (sonuc and "slug" in sonuc):
|
||||||
|
return None # "modeli" denmemis ve ad tek modele oturmuyor: komut degil, modele gider
|
||||||
|
if not sonuc:
|
||||||
|
if ortada:
|
||||||
|
return None
|
||||||
|
# Acik komut ("modeli ... yap") ama ad bulunamadi: model "kopru yapacak" diye yanlis soz vermesin.
|
||||||
|
log(f"[{n} {gelen}] model komutu: ad bulunamadi {komut['ad']}")
|
||||||
|
return (f"\"{' '.join(komut['ad'])}\" adına uyan bir model bulamadım, model değişmedi; sohbetin modeli hâlâ "
|
||||||
|
f"{model_adi(etkin)}. Adını biraz farklı söyler misin?")
|
||||||
if "adaylar" in sonuc:
|
if "adaylar" in sonuc:
|
||||||
soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: "
|
soru = ("Hangi modeli istediğini tek modele bağlayamadım, model değişmedi. En yakınlar: "
|
||||||
+ ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?")
|
+ ", ".join(sonuc["adaylar"]) + ". Adını biraz daha tam söyler misin?")
|
||||||
@@ -1043,6 +1217,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return soru
|
return soru
|
||||||
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; "
|
model_gecis.metni_degistir(req, sira, "[Köprü notu: kullanıcı model değiştirmek istedi ama ad belirsizdi; "
|
||||||
"model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]")
|
"model değişmedi. İşine devam et; cevabının sonunda şunu ilet: " + soru + "]")
|
||||||
|
self.komut_govdeyi_degistirdi = True
|
||||||
return None
|
return None
|
||||||
hedef = sonuc["slug"]
|
hedef = sonuc["slug"]
|
||||||
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
|
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
|
||||||
@@ -1054,9 +1229,12 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
|
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
|
||||||
if ortada:
|
if ortada:
|
||||||
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
|
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
|
||||||
|
self.komut_govdeyi_degistirdi = True
|
||||||
return None
|
return None
|
||||||
|
# Listede ayni modeli yeniden secmek yonlendirmeyi kaldirmaz (Codex ayni adi gonderir); geri donus yolu soylenir.
|
||||||
|
donus = f" Listede seçili modele dönmek için \"modeli {model_adi(gelen)} yap\" de." if hedef != gelen else ""
|
||||||
return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. "
|
return (f"Sohbetin modeli artık {sonuc['ad']}. Önceki model {model_adi(etkin)} idi. "
|
||||||
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.")
|
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}")
|
||||||
|
|
||||||
def _responses(self, n, path, body):
|
def _responses(self, n, path, body):
|
||||||
try:
|
try:
|
||||||
@@ -1070,6 +1248,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)
|
once = len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)
|
||||||
sohbet = str(req.get("prompt_cache_key") or "")
|
sohbet = str(req.get("prompt_cache_key") or "")
|
||||||
if CFG.get("sesle_model", True) and sohbet:
|
if CFG.get("sesle_model", True) and sohbet:
|
||||||
|
self.komut_govdeyi_degistirdi = False
|
||||||
try:
|
try:
|
||||||
yerel = self._model_komutu(n, req, sohbet, gelen)
|
yerel = self._model_komutu(n, req, sohbet, gelen)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1077,8 +1256,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
|
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
|
||||||
if yerel is not None:
|
if yerel is not None:
|
||||||
return self._yerel_yanit(gelen, yerel)
|
return self._yerel_yanit(gelen, yerel)
|
||||||
|
if self.komut_govdeyi_degistirdi:
|
||||||
|
govde_degisti = True # komut mesaji nota cevrildi: abonelik yolunda da degismis govde gider
|
||||||
try:
|
try:
|
||||||
if model_gecis.gecmisi_temizle(req, bilinen_modeller()):
|
if model_gecis.gecmisi_temizle(req, bilinen_modeller(), YONLENDIRME.al(sohbet, gelen) or gelen):
|
||||||
govde_degisti = True
|
govde_degisti = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
|
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
|
||||||
@@ -1090,7 +1271,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
||||||
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
||||||
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
|
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
|
||||||
del req["input"][model_gecis.son_sira(req["input"]):]
|
del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir
|
||||||
govde_degisti = True
|
govde_degisti = True
|
||||||
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
|
log(f"[{n} {slug}] tur ortasinda gelen nokta cikarildi; calisan is kesilmedi")
|
||||||
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
|
if slug != gelen or once != (len(req.get("input") or []), json.dumps((req.get("input") or [None])[-3:], ensure_ascii=False, sort_keys=True)):
|
||||||
@@ -1099,7 +1280,21 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
# Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da
|
# Model degisimini uygulamak icin yazilan tek "." modele gonderilmez: hicbir model onu onay ya da
|
||||||
# istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur.
|
# istek sanip is yapmaz, yavas modelde asil soru beklemez. Secilen modelin adiyla aninda donulur.
|
||||||
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
|
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
|
||||||
return self._yerel_yanit(gelen, f"{model_adi(slug)} hazır. Sohbetin modeli artık {model_adi(slug)}.")
|
return self._yerel_yanit(gelen, f"{model_adi(slug)} seçildi. Sohbetin modeli artık {model_adi(slug)}.")
|
||||||
|
# Is hatti talimati yalniz masaustu uygulamasindan gelen istege eklenir; CLI hic gormez (is_hatti.py).
|
||||||
|
kaynak = self.headers.get("originator") or ""
|
||||||
|
if kaynak and kaynak not in GORULEN_ISTEMCI:
|
||||||
|
GORULEN_ISTEMCI.add(kaynak)
|
||||||
|
log(f"istemci goruldu: {kaynak}")
|
||||||
|
try:
|
||||||
|
# Model, sohbetin modelinin soylenerek degistirilebildigini bilsin; yoksa "degistiremem" diyor.
|
||||||
|
if CFG.get("sesle_model", True) and CFG.get("model_bilgi", True) and sohbet and model_gecis.bilgi_ekle(req):
|
||||||
|
govde_degisti = True
|
||||||
|
if is_hatti.masaustu_mu(kaynak) and is_hatti.ekle(
|
||||||
|
req, is_hatti.talimat(CFG.get("aiturk_hat"), HAT_KOMUT[0], sohbet)):
|
||||||
|
govde_degisti = True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"[{n} {slug}] is hatti talimati atlandi: {type(e).__name__}: {e}")
|
||||||
# Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir.
|
# Ayni sohbetin onceki cevabi yarida kesildiyse (yarim.py) metni bu istege not olarak eklenir.
|
||||||
yarim_eklenen = 0
|
yarim_eklenen = 0
|
||||||
if CFG.get("yarim_cevap", True):
|
if CFG.get("yarim_cevap", True):
|
||||||
@@ -1119,16 +1314,28 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
ozet_acik = ozet_cfg.get("enabled", False)
|
ozet_acik = ozet_cfg.get("enabled", False)
|
||||||
catalog = CFG.get("nine_router_catalog") or {}
|
catalog = CFG.get("nine_router_catalog") or {}
|
||||||
prefix = catalog.get("prefix", "9r/")
|
prefix = catalog.get("prefix", "9r/")
|
||||||
if mapping is None and catalog.get("enabled") and slug.startswith(prefix):
|
if mapping is None and prefix and slug.startswith(prefix):
|
||||||
# Newly added API models can be selected before the background refresh.
|
if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES:
|
||||||
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
|
# Newly added API models can be selected before the background refresh.
|
||||||
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
|
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
|
||||||
|
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
|
||||||
|
else:
|
||||||
|
# Rotasi kalmamis eski ad chatgpt.com'a baska bir adla gonderilmez ("model desteklenmiyor" donuyordu).
|
||||||
|
log(f"[{n} {slug}] bu on ekin rotasi yok; istek gonderilmedi")
|
||||||
|
return self._send_json(503, {"error": {"message": "Bu model artik bu koprude tanimli degil; listeden baska bir model secin."}})
|
||||||
ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler
|
ide_prefix, ide_route = "ide/", "aiturk-ide" # load_ide_catalog ile ayni degerler
|
||||||
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
|
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
|
||||||
mapping = (ide_route, slug[len(ide_prefix):], None)
|
mapping = (ide_route, slug[len(ide_prefix):], None)
|
||||||
if mapping is None and slug.startswith(ide_prefix):
|
if mapping is None and slug.startswith(ide_prefix):
|
||||||
# Never send an IDE model to chatgpt.com under another name.
|
# Never send an IDE model to chatgpt.com under another name.
|
||||||
log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok")
|
log(f"[{n} IDE {slug}] Codex Baglama kapali ya da IDE anahtari yok")
|
||||||
|
# 503 donmek Codex'i sessizce yeniden denetiyor ve tur cevapsiz bitiyordu; sesli asistan da "bakiyorum"
|
||||||
|
# deyip duruyordu (olcum 11 Ekim 2026, sunucu gecisi sirasinda). Kullanici durumu duysun.
|
||||||
|
if CFG.get("kesinti_mesaji", True):
|
||||||
|
return self._yerel_yanit(gelen, "AITURK modellerine şu an ulaşılamıyor (AITURK sunucusu cevap vermiyor ya da "
|
||||||
|
"bağlantı kapalı); bu model şimdi cevap veremez. Beklemek istemezsen Codex'in kendi "
|
||||||
|
"modeline geç: \"modeli gpt 6 astra yap\" de. AITURK geri gelince tekrar "
|
||||||
|
"\"modeli … yap\" diyerek dönebilirsin.")
|
||||||
return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}})
|
return self._send_json(503, {"error": {"message": "AITURK IDE Codex Baglama kapali ya da IDE oturumu bulunamadi; bu model su an kullanilamiyor."}})
|
||||||
if mapping is None:
|
if mapping is None:
|
||||||
# abonelik modeli: chatgpt.com'a oldugu gibi
|
# abonelik modeli: chatgpt.com'a oldugu gibi
|
||||||
@@ -1200,7 +1407,11 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
|
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
|
||||||
via_router = route_name in (catalog.get("route", "9router"), ide_route)
|
via_router = route_name in (catalog.get("route", "9router"), ide_route)
|
||||||
route = ROUTES[route_name]
|
route = ROUTES.get(route_name)
|
||||||
|
if route is None:
|
||||||
|
# Katalog yenilemesi rotayi bu istek hazirlanirken kaldirdi (baglama kapandi ya da IDE oturumu gitti).
|
||||||
|
log(f"[{n} {slug}] rota artik yok: {route_name}")
|
||||||
|
return self._send_json(503, {"error": {"message": "Bu modelin baglantisi su an kapali; birazdan yeniden deneyin ya da baska bir model secin."}})
|
||||||
tag = f"[{n} {route_name.upper()} {slug}->{target}]"
|
tag = f"[{n} {route_name.upper()} {slug}->{target}]"
|
||||||
req["model"] = target
|
req["model"] = target
|
||||||
req.pop("client_metadata", None)
|
req.pop("client_metadata", None)
|
||||||
@@ -1215,16 +1426,20 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if ozet_tetik:
|
if ozet_tetik:
|
||||||
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
|
ozet_kopru.ozet_istegi_hazirla(req, ozet_cfg.get("istem"))
|
||||||
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
|
log(f"{tag} ozet kopru: sikistirma istegi duz metin ozet istegine cevrildi (girdi={len(req.get('input', []))} oge)")
|
||||||
rewriter = None
|
|
||||||
if flat if flat is not None else route.get("flatten_tools"):
|
|
||||||
custom = flatten_request(req)
|
|
||||||
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
|
|
||||||
fix_tool_ids(req)
|
|
||||||
if via_router and CFG.get('resim_sadelestir', True):
|
if via_router and CFG.get('resim_sadelestir', True):
|
||||||
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
|
# Uretilen resim (≈3 MB) 9Router cevirisinde duz yaziya donup sohbeti 3M token'a cikariyordu (24 Eylul 2026).
|
||||||
|
# Duzlestirmeden ONCE: kimliksiz arac ciktisi orada duz metne cevrilir ve icindeki resim taninmaz olur.
|
||||||
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
|
cikan, kazanc = resimleri_sadelestir(req, gorsel=bool((IDE_BILGI.get(slug) or {}).get("gorsel")))
|
||||||
if cikan:
|
if cikan:
|
||||||
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
|
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
|
||||||
|
rewriter = None
|
||||||
|
if flat if flat is not None else route.get("flatten_tools"):
|
||||||
|
custom = flatten_request(req)
|
||||||
|
duzelen = arac_semalarini_duzelt(req)
|
||||||
|
if duzelen:
|
||||||
|
log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)")
|
||||||
|
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
|
||||||
|
fix_tool_ids(req)
|
||||||
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
|
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
|
||||||
merged = coalesce_context(req)
|
merged = coalesce_context(req)
|
||||||
if merged:
|
if merged:
|
||||||
@@ -1242,9 +1457,12 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
|
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
|
||||||
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
|
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
|
||||||
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
|
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
|
||||||
pencere_biliniyor = "varsayilan" not in ((IDE_BILGI.get(slug) or {}).get("kaynak") or "varsayilan")
|
# Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir).
|
||||||
|
pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere)
|
||||||
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
|
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
|
||||||
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere)
|
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude"))))
|
||||||
|
if rewriter is not None:
|
||||||
|
rewriter.dolu = tahmin >= hedef_pencere * 0.7
|
||||||
if kirpilan:
|
if kirpilan:
|
||||||
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
|
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
|
||||||
if not sigdi:
|
if not sigdi:
|
||||||
@@ -1267,8 +1485,8 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
|
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
|
||||||
except urllib.error.HTTPError as e:
|
except urllib.error.HTTPError as e:
|
||||||
errbody = e.read()[:400]
|
errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir
|
||||||
last = (e.code, errbody)
|
last = (e.code, errbody[:400])
|
||||||
etxt = errbody.decode("utf-8", "replace")
|
etxt = errbody.decode("utf-8", "replace")
|
||||||
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
|
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
|
||||||
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
|
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
|
||||||
@@ -1289,6 +1507,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
return self._send_sse_failed("context_length_exceeded", etxt[:300])
|
return self._send_sse_failed("context_length_exceeded", etxt[:300])
|
||||||
self.send_response(e.code)
|
self.send_response(e.code)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
|
bekle = e.headers.get("Retry-After") if e.headers is not None else None
|
||||||
|
if bekle:
|
||||||
|
self.send_header("Retry-After", str(bekle)) # 429'da ne kadar beklenecegi istemciye ulassin
|
||||||
self.send_header("Content-Length", str(len(errbody)))
|
self.send_header("Content-Length", str(len(errbody)))
|
||||||
self.end_headers(); self.wfile.write(errbody); return
|
self.end_headers(); self.wfile.write(errbody); return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1296,6 +1517,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
if combo and is_timeout(e):
|
if combo and is_timeout(e):
|
||||||
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
|
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
|
||||||
break
|
break
|
||||||
|
if combo and kopma_mi(e):
|
||||||
|
log(f"{tag} combo istegi gonderildikten sonra baglanti koptu, tekrar gonderilmiyor (calisiyor olabilir): {e}")
|
||||||
|
break
|
||||||
log(f"{tag} deneme {attempt}: {e}")
|
log(f"{tag} deneme {attempt}: {e}")
|
||||||
time.sleep(min(2 * attempt, 8)); continue
|
time.sleep(min(2 * attempt, 8)); continue
|
||||||
# Headers go to the client from here on: a cut stream is never resent.
|
# Headers go to the client from here on: a cut stream is never resent.
|
||||||
|
|||||||
@@ -148,7 +148,11 @@ def adlandir(girdiler, dolu=(), turler=None):
|
|||||||
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
|
||||||
@@ -187,13 +191,15 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
|
|||||||
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} 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=()):
|
||||||
"""Ozgun abonelik modelleri oldugu gibi kalir: sirasi, priority degeri ve adi degismez,
|
"""Ozgun abonelik modelleri oldugu gibi kalir: sirasi, priority degeri ve adi degismez,
|
||||||
listenin basinda durur. Ek modeller onlarin ARKASINA eklenir: combo A-Z, sonra
|
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)"
|
saglayici A-Z / model A-Z. Ozgun bir modelle ayni adi tasiyan ek model " (AITURK)"
|
||||||
ekiyle ayirt edilir; secicide ozgunun yerine gecmis gibi gorunmez.
|
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.
|
||||||
bilgiler = {m["slug"]: m for m in ekler}
|
ozgun_kimlikler: ust listeden (chatgpt.com) gelen kimlikler. Ayarda ayni kimlik ek model diye yazilmis
|
||||||
|
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]
|
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]
|
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
|
def yalin(ad): # "GPT-6-1-Sol" ile "GPT-6.1-Sol" ayni ad sayilir
|
||||||
@@ -348,5 +354,6 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
|
|||||||
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
|
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"],
|
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
|
||||||
bilgi["arac"], bilgi["kaynak"])
|
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)
|
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
|
||||||
@@ -21,17 +21,31 @@ import time
|
|||||||
|
|
||||||
DOSYA = "model-yonlendirme.json"
|
DOSYA = "model-yonlendirme.json"
|
||||||
OMUR_SN = 7 * 24 * 3600
|
OMUR_SN = 7 * 24 * 3600
|
||||||
|
YENILE_SN = 24 * 3600
|
||||||
EN_COK_KAYIT = 200
|
EN_COK_KAYIT = 200
|
||||||
EN_COK_KELIME = 14
|
EN_COK_KELIME = 14
|
||||||
_KILIT = threading.Lock()
|
_KILIT = threading.Lock()
|
||||||
|
|
||||||
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
|
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
|
||||||
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
|
# 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")
|
SORU = ("hangi", "hangisi", "kim", "kimdir")
|
||||||
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
|
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",
|
"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",
|
"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"}
|
"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
|
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
|
||||||
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
|
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
|
||||||
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
|
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
|
||||||
@@ -94,18 +108,68 @@ def son_kullanici_metni(req):
|
|||||||
return metin.strip(), sira, False
|
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):
|
def komut_coz(metin):
|
||||||
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
|
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
|
||||||
kelimeler = re.findall(r"[a-z0-9]+", duz(metin))
|
kelimeler = _harfleri_birlestir(re.findall(r"[a-z0-9]+", duz(metin)))
|
||||||
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
|
if not kelimeler or len(kelimeler) > EN_COK_KELIME:
|
||||||
return None
|
return None
|
||||||
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
|
if not any(k.startswith("model") for k in kelimeler):
|
||||||
return {"tur": "sor"}
|
# "modeli" denmeden soylenen komut ("Seed iki nokta bir turbo yap", "gemini flash low'a gec"): yalniz cumle
|
||||||
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
|
# bir komut fiiliyle bitiyorsa ve ad bir modele TAM uyuyorsa komut sayilir (kesin=False: cagiran, ad
|
||||||
if fiil:
|
# bulunamazsa mesaji komut saymaz, modele birakir).
|
||||||
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
|
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)]
|
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
|
||||||
return {"tur": "gec", "ad": ad} if ad else None
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -131,15 +195,33 @@ def eslestir(ad, modeller):
|
|||||||
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
|
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))
|
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
|
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
|
||||||
puanli.append((tutan, -len(parcalar), slug, gorunen))
|
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:
|
if not puanli:
|
||||||
return None
|
return None
|
||||||
en = max(p[0] for p in puanli)
|
en = max(p[0] for p in puanli)
|
||||||
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
|
harfler = [s for s in ad if not s.isdigit()]
|
||||||
return None # sozun cogu hicbir modele uymuyor: komut degil
|
en_harf = max(p[4] for p in puanli)
|
||||||
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
|
if en_harf < len(harfler):
|
||||||
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
|
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]}
|
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]]}
|
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||||||
|
|
||||||
|
|
||||||
@@ -176,14 +258,19 @@ def metni_degistir(req, sira, yeni):
|
|||||||
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
|
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
|
||||||
|
|
||||||
|
|
||||||
def gecmisi_temizle(req, modeller):
|
def gecmisi_temizle(req, modeller, etkin=None):
|
||||||
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
|
"""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).
|
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."""
|
Doner: degistirilen mesaj sayisi."""
|
||||||
girdi = req.get("input")
|
girdi = req.get("input")
|
||||||
if not isinstance(girdi, list):
|
if not isinstance(girdi, list):
|
||||||
return 0
|
return 0
|
||||||
son = son_sira(girdi)
|
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
|
n = 0
|
||||||
for i, oge in enumerate(girdi):
|
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"):
|
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||||||
@@ -194,12 +281,39 @@ def gecmisi_temizle(req, modeller):
|
|||||||
komut = komut_coz(metin)
|
komut = komut_coz(metin)
|
||||||
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
|
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
|
||||||
if sonuc and "slug" in sonuc:
|
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; "
|
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.]")
|
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
|
||||||
n += 1
|
n += 1
|
||||||
return n
|
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):
|
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 "
|
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"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
|
||||||
@@ -244,6 +358,10 @@ class Yonlendirme:
|
|||||||
del self.veri[sohbet]
|
del self.veri[sohbet]
|
||||||
self._yaz()
|
self._yaz()
|
||||||
return None
|
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"]
|
return k["hedef"]
|
||||||
|
|
||||||
def koy(self, sohbet, gelen, hedef, simdi=None):
|
def koy(self, sohbet, gelen, hedef, simdi=None):
|
||||||
|
|||||||
@@ -266,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 ----
|
||||||
@@ -300,6 +301,8 @@ class DonguFreni:
|
|||||||
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 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:
|
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
|
||||||
@@ -307,7 +310,7 @@ class DonguFreni:
|
|||||||
self.kayit[anahtar] = zamanlar
|
self.kayit[anahtar] = zamanlar
|
||||||
self.ust_uste[anahtar] = self.ust_uste.get(anahtar, 0) + 1
|
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)
|
self.ust_uste.pop(k, None)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asma
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ def _oku(yol):
|
|||||||
|
|
||||||
|
|
||||||
def _yaz(yol, veri):
|
def _yaz(yol, veri):
|
||||||
gecici = yol + ".tmp"
|
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:
|
with open(gecici, "w", encoding="utf-8") as f:
|
||||||
json.dump(veri, f, ensure_ascii=False)
|
json.dump(veri, f, ensure_ascii=False)
|
||||||
os.replace(gecici, yol)
|
os.replace(gecici, yol)
|
||||||
@@ -86,7 +87,11 @@ def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir
|
|||||||
simdi = time.time() if simdi is None else simdi
|
simdi = time.time() if simdi is None else simdi
|
||||||
yol = os.path.join(home, NANO_DOSYA) if home else ""
|
yol = os.path.join(home, NANO_DOSYA) if home else ""
|
||||||
onbellek = _oku(yol) if yol else {}
|
onbellek = _oku(yol) if yol else {}
|
||||||
if onbellek.get("modeller") and simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN:
|
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"]
|
return onbellek["modeller"]
|
||||||
if not ag:
|
if not ag:
|
||||||
return onbellek.get("modeller") or {}
|
return onbellek.get("modeller") or {}
|
||||||
@@ -141,12 +146,27 @@ def ogren_aracsiz(home, kimlik):
|
|||||||
return True
|
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):
|
def tasma_mi(metin):
|
||||||
return bool(_TASMA.search(metin or ""))
|
return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
|
||||||
|
|
||||||
|
|
||||||
def sinir_oku(metin):
|
def sinir_oku(metin):
|
||||||
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
|
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
|
||||||
|
if hiz_siniri_mi(metin):
|
||||||
|
return None
|
||||||
for kalip in _SINIR:
|
for kalip in _SINIR:
|
||||||
m = kalip.search(metin or "")
|
m = kalip.search(metin or "")
|
||||||
if m and int(m.group(1)) >= TABAN:
|
if m and int(m.group(1)) >= TABAN:
|
||||||
@@ -227,6 +247,29 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
|
|||||||
return sonuc
|
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):
|
def kisa(pencere):
|
||||||
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
|
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
|
||||||
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
|
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
|
||||||
@@ -243,8 +286,38 @@ KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
|
|||||||
KIRP_BAS = KIRP_SON = 1500
|
KIRP_BAS = KIRP_SON = 1500
|
||||||
|
|
||||||
|
|
||||||
def tahmini_token(req):
|
# Claude ayni metni daha cok tokena boler. Olcum (11 Ekim 2026): 607 KB'lik gercek Codex istegi 3,2 oraniyla
|
||||||
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / BAYT_BOLU_TOKEN)
|
# 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):
|
def _metin_parcalari(req):
|
||||||
@@ -254,8 +327,9 @@ def _metin_parcalari(req):
|
|||||||
girdi = req.get("input")
|
girdi = req.get("input")
|
||||||
if not isinstance(girdi, list):
|
if not isinstance(girdi, list):
|
||||||
return []
|
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"
|
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"), default=-1)
|
and it.get("type", "message") == "message" and not _kopru_mesaji(it)), default=-1)
|
||||||
parcalar = []
|
parcalar = []
|
||||||
for i, it in enumerate(girdi):
|
for i, it in enumerate(girdi):
|
||||||
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
|
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
|
||||||
@@ -271,22 +345,24 @@ def _metin_parcalari(req):
|
|||||||
return parcalar
|
return parcalar
|
||||||
|
|
||||||
|
|
||||||
def sigdir(req, pencere_token):
|
def sigdir(req, pencere_token, oran=None):
|
||||||
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
|
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
|
||||||
sinir = int(pencere_token * DOLULUK)
|
sinir = int(pencere_token * DOLULUK)
|
||||||
tahmin = tahmini_token(req)
|
tahmin = tahmini_token(req, oran)
|
||||||
if not pencere_token or tahmin <= sinir:
|
if not pencere_token or tahmin <= sinir:
|
||||||
return 0, True, tahmin
|
return 0, True, tahmin
|
||||||
kirpilan = 0
|
kirpilan = 0
|
||||||
parcalar = sorted(_metin_parcalari(req), key=lambda sa: len(sa[0][sa[1]]), reverse=True)
|
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:
|
for sahip, anahtar in parcalar:
|
||||||
metin = sahip[anahtar]
|
metin = sahip[anahtar]
|
||||||
if len(metin) < KIRP_ESIK:
|
if len(metin) < KIRP_ESIK:
|
||||||
break
|
continue
|
||||||
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
|
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:])
|
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
|
||||||
kirpilan += 1
|
kirpilan += 1
|
||||||
tahmin = tahmini_token(req)
|
tahmin = tahmini_token(req, oran)
|
||||||
if tahmin <= sinir:
|
if tahmin <= sinir:
|
||||||
return kirpilan, True, tahmin
|
return kirpilan, True, tahmin
|
||||||
return kirpilan, tahmin <= sinir, tahmin
|
return kirpilan, tahmin <= sinir, tahmin
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
|
|||||||
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:
|
||||||
@@ -72,7 +73,7 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
|
|||||||
kalan = tut_bayt - sum(_boyut(p) for i in koru for p in inp[i].get('content') or [] if _medya_mi(p))
|
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):
|
for i in range(len(inp) - 1, -1, -1):
|
||||||
it = inp[i]
|
it = inp[i]
|
||||||
if i in koru or not (isinstance(it, dict) and it.get('type') == 'message' and it.get('role') == 'user'):
|
if i in koru or not (isinstance(it, dict) and it.get('type', 'message') == 'message' and it.get('role') == 'user'):
|
||||||
continue
|
continue
|
||||||
icerik = it.get('content')
|
icerik = it.get('content')
|
||||||
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
|
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
|
||||||
|
|||||||
@@ -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, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
|
"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": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
|
"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()
|
||||||
@@ -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()
|
||||||
@@ -8,7 +8,8 @@ from pathlib import Path
|
|||||||
import pencere
|
import pencere
|
||||||
|
|
||||||
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 = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti"}
|
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]
|
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
|
||||||
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
|
||||||
@@ -31,8 +32,11 @@ class Ek(unittest.TestCase):
|
|||||||
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
|
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
|
||||||
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
|
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
|
||||||
out = feed(flat, hedef="m", aracsiz=seen.append)
|
out = feed(flat, hedef="m", aracsiz=seen.append)
|
||||||
self.assertEqual(out[0]["type"], "response.failed")
|
# A permanent error ends the turn with a visible answer instead of a silent retry.
|
||||||
self.assertIn("araç çağrısını desteklemiyor", out[0]["response"]["error"]["message"])
|
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"])
|
self.assertEqual(seen, ["m"])
|
||||||
home = tempfile.mkdtemp()
|
home = tempfile.mkdtemp()
|
||||||
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
|
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
|
||||||
@@ -60,6 +64,25 @@ class Ek(unittest.TestCase):
|
|||||||
self.assertIn("çıkarıldı (köprü)", req["input"][1]["content"][0]["text"])
|
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):
|
class Nokta(unittest.TestCase):
|
||||||
def user(self, content):
|
def user(self, content):
|
||||||
return {"type": "message", "role": "user", "content": content}
|
return {"type": "message", "role": "user", "content": content}
|
||||||
|
|||||||
@@ -29,18 +29,28 @@ class Komut(unittest.TestCase):
|
|||||||
def test_spoken_names_resolve_to_one_model(self):
|
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"),
|
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 Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
|
||||||
("modeli klod opus yap", "ide/9router-cc-claude-opus-5-5"),
|
("modeli klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
|
||||||
("modeli opusa geç", "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 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")]:
|
("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)
|
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
|
||||||
|
|
||||||
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
|
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.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):
|
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",
|
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
|
||||||
"modeli değiştir", "hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
|
"hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
|
||||||
"model " + "çok uzun bir cümle " * 5 + "yap"):
|
"model " + "çok uzun bir cümle " * 5 + "yap"):
|
||||||
self.assertFalse(pick(text) and "slug" in pick(text), text)
|
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("seçili model hangisi"), {"tur": "sor"})
|
||||||
@@ -77,7 +87,7 @@ class TurOrtasi(unittest.TestCase):
|
|||||||
class Gecmis(unittest.TestCase):
|
class Gecmis(unittest.TestCase):
|
||||||
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
|
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"),
|
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
|
||||||
voice("modeli opus yap"), user("modeli gpt 6 astra yap")]}
|
voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
|
||||||
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
|
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.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.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
|
||||||
@@ -86,6 +96,44 @@ class Gecmis(unittest.TestCase):
|
|||||||
self.assertEqual(g.gecmisi_temizle(req, MODELS), 0)
|
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):
|
class Yon(unittest.TestCase):
|
||||||
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
|
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
|
||||||
with tempfile.TemporaryDirectory() as home:
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ 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", "ARAC_OGELERI"} 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)
|
||||||
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": pencere}
|
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)
|
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
|
||||||
|
|
||||||
NANO = pencere.nano_sadelestir({"data": [
|
NANO = pencere.nano_sadelestir({"data": [
|
||||||
@@ -196,9 +196,13 @@ class GatewayErrors(unittest.TestCase):
|
|||||||
adapter = env["SseRewriter"](set())
|
adapter = env["SseRewriter"](set())
|
||||||
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
|
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
|
||||||
out = parse(adapter.feed(raw) + adapter.flush())
|
out = parse(adapter.feed(raw) + adapter.flush())
|
||||||
self.assertEqual(out[0]["type"], "response.failed")
|
self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
|
||||||
self.assertEqual(out[0]["response"]["error"]["code"], "insufficient_quota")
|
self.assertIn("kredi ya da kota", out[1]["delta"])
|
||||||
self.assertIn("upstream 402", out[0]["response"]["error"]["message"])
|
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.
|
# A transient error is left alone so that Codex retries it.
|
||||||
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
|
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
|
||||||
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
|
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
|
||||||
@@ -225,5 +229,25 @@ class Images(unittest.TestCase):
|
|||||||
self.assertEqual(resim_context.resimleri_sadelestir(big, tut_bayt=500, gorsel=True)[0], 2) # over budget
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Izleyici:
|
|||||||
|
|
||||||
def feed(self, chunk):
|
def feed(self, chunk):
|
||||||
self.buf += 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:
|
while b"\n\n" in self.buf:
|
||||||
blok, self.buf = self.buf.split(b"\n\n", 1)
|
blok, self.buf = self.buf.split(b"\n\n", 1)
|
||||||
if b"response.output_text.delta" in blok:
|
if b"response.output_text.delta" in blok:
|
||||||
@@ -48,7 +50,15 @@ class Izleyici:
|
|||||||
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
|
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
|
||||||
self.parcalar.append(d["delta"])
|
self.parcalar.append(d["delta"])
|
||||||
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
|
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
|
||||||
self.bitti = True
|
# 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
|
@property
|
||||||
def metin(self):
|
def metin(self):
|
||||||
@@ -89,7 +99,12 @@ def uygula(req, anahtar, simdi=None):
|
|||||||
girdi = req.get("input")
|
girdi = req.get("input")
|
||||||
if not kayit or not isinstance(girdi, list) or not girdi:
|
if not kayit or not isinstance(girdi, list) or not girdi:
|
||||||
return 0
|
return 0
|
||||||
son = girdi[-1]
|
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"):
|
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
|
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
|
||||||
metin = kayit["metin"]
|
metin = kayit["metin"]
|
||||||
|
|||||||
Reference in New Issue
Block a user