Compare commits

..
Author SHA1 Message Date
yilsemandClaude Opus 5.5 48103b9ad3 feat(codex-bridge): the AITURK work line is part of Codex Baglama
ci / mobile-link-windows (push) Successful in 33s
ci / approval-gate-windows (push) Successful in 2m40s
The instruction is added for Codex Desktop whenever this computer has an AITURK
command that can send work (aiturk is); no separate switch, no bridge.json entry.
The bridge fills in the chat id; the command runs in Codex's own folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 03:31:49 +03:00
yilsemandClaude Opus 5.5 615fedde01 fix(codex-bridge): say so when AITURK is unreachable instead of a silent 503
ci / mobile-link-windows (push) Successful in 47s
ci / approval-gate-windows (push) Successful in 2m56s
During the server cutover the gateway was down; the bridge answered 503, Codex retried silently, the turn ended
empty and the voice assistant kept saying it was checking. The user now hears that AITURK is unreachable and how
to switch to a subscription model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 02:01:21 +03:00
yilsemandClaude Opus 5.5 a46ff51019 feat(codex-bridge): natural model command, spelled-out names, any gateway model, clear miss
ci / mobile-link-windows (push) Successful in 39s
ci / approval-gate-windows (push) Successful in 2m59s
Live voice chat 01a12804: the user said "Seed iki nokta bir turbo yap" (no "modeli", name spelled out) and the
model was in the gateway but not in Codex's list; the main model then promised the bridge would do it.
A sentence ending in a command verb whose name fits exactly one model is now a command, single letters are
joined, a name missing from Codex's list is looked up in the gateway's full model list, and an explicit
command whose name fits nothing is answered by the bridge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:58:58 +03:00
yilsemandClaude Opus 5.5 8a612beba7 feat(codex-bridge): tell the model the chat model can be changed by saying so; a nameless command asks which
ci / approval-gate-windows (push) Successful in 2m51s
ci / mobile-link-windows (push) Successful in 35s
Measured in a live voice chat: asked whether it could change the model, Opus answered "I cannot change this
chat's model here". Every request now carries one short developer note, and "modeli değiştir" without a
name is answered by the bridge with how to say it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:55:34 +03:00
yilsemandClaude Opus 5.5 7c0dc467d5 fix(codex-bridge): remaining audit findings (routing, trimming, retries, model list fallback, logging)
ci / mobile-link-windows (push) Successful in 34s
ci / approval-gate-windows (push) Successful in 2m39s
Second batch from the ten read-only audits; 126 bridge tests. Among them: a spoken number
("opus beş beş") resolves, a used route no longer expires after 7 days, the local reply says how to
go back, an unrouted 9r/ name gets a local 503 instead of going to chatgpt.com, a combo request is not
resent after the upstream closed it, the last model list is served when chatgpt.com is unreachable,
trimming keeps the real last user message, Claude-member combos use the Claude estimate, CRLF SSE is
handled, log lines carry the date and the status endpoint reports a code fingerprint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:50:08 +03:00
yilsemandClaude Opus 5.5 e2793fa38f fix(codex-bridge): audit findings: visible permanent errors, stricter model command, unbuffered stream
ci / mobile-link-windows (push) Successful in 36s
ci / approval-gate-windows (push) Successful in 2m41s
From ten independent read-only audits (11 Oct 2026), verified against the code:
- a permanent gateway error (no tools, 401/402/403, bare upstream 400) ends the turn with a visible
  assistant message instead of a silent retry and an empty turn;
- "modeli ... yapma", "X modeli için test yap" and general "hangi model ..." questions are no longer
  commands; a name whose version does not fit, or that several models share, is asked back;
- the stream is forwarded as it arrives (read1) and an upstream drop is no longer logged as a client
  stop nor saved as an interrupted answer;
- a lone "." joining a running turn is removed without the hook messages after it;
- the interrupted-answer note finds the user message behind hook messages and detects completion
  by event type; a per-minute rate limit is not learned as a context window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:23:07 +03:00
yilsemandClaude Opus 5.5 1e2b8e2b9c fix(codex-bridge): flatten top-level oneOf/anyOf/allOf in tool schemas for external models
ci / mobile-link-windows (push) Successful in 31s
ci / approval-gate-windows (push) Successful in 2m39s
Since bridge 7 external models get Codex's classic tools. One desktop tool has a top-level union schema,
which Claude rejects ("input_schema does not support oneOf, allOf, or anyOf at the top level"), so every
Opus request from the desktop app failed with a bare "upstream 400". The earlier context-size diagnosis
was wrong: the request still failed after compaction (239 KB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:08:27 +03:00
yilsemandClaude Opus 5.5 1445e204da feat(codex-bridge): handoff instruction for Codex Desktop only (off by default)
ci / mobile-link-windows (push) Successful in 32s
ci / approval-gate-windows (push) Successful in 2m41s
User decision: the "larger work goes to AITURK" instruction lives only in what Codex IDE sees; the CLI must
not know it exists. The bridge adds it in flight by the originator header (measured: Codex Desktop, codex-tui,
codex_exec) and only when a send command is configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 01:05:00 +03:00
yilsemandClaude Opus 5.5 d9a7661281 fix(codex-bridge): estimate Claude requests more strictly; report an unexplained 400 on a near-full request
ci / mobile-link-windows (push) Successful in 32s
ci / approval-gate-windows (push) Successful in 2m42s
A 607 KB Codex request was estimated at 189776 tokens (window 200000), sent untrimmed and rejected by
Claude; the gateway only said "upstream 400", so Codex saw a dropped stream. Claude targets now use
2.6 bytes/token, and that bare 400 on a request filling most of the window becomes context_length_exceeded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:45:58 +03:00
yilsemandClaude Opus 5.5 959f5f1459 feat(codex-bridge): change the chat model by saying so; bridge 8, IDE 1.0.0-beta.27
ci / mobile-link-windows (push) Successful in 55s
ci / approval-gate-windows (push) Successful in 2m42s
A model picked on the phone only arrives with a typed message that starts a turn. The bridge now
handles "modeli X yap" itself: per-chat routing, same-turn handover when a model is working,
truthful answer to "which model", and a lone "." joining a running turn no longer cuts the work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:05:18 +03:00
yilsemandClaude Opus 5.5 acf88f8314 feat(codex-link): bridge 7 - external models work like subscription models
ci / mobile-link-windows (push) Successful in 43s
ci / approval-gate-windows (push) Successful in 2m51s
Measured with the unmodified Codex binary on 10 Oct 2026:
- real context windows per model (NanoGPT list, local table, combo = its
  smallest member, capped at the summarizer's window) so Codex compacts by
  itself when switching to a smaller model; an overflow guard trims
  oversized old parts and reports what still does not fit
- classic tools instead of code mode for external models: GLM 0/3 -> 3/3,
  Qwen 0/3 -> 2/3 on simple tool tasks, Claude/cx/Gemini unchanged
- phase (commentary / final_answer) on external assistant messages
- an answer cut by the client is carried to the next turn once
- images stay for vision-capable targets
- summary brake stops tight loops, not frequent compaction
- permanent gateway errors and tool-less targets fail at once with a
  readable message instead of five reconnects
- a lone "." (used to apply a model change in voice) is answered locally
- the bridge listens within seconds; the IDE waits 45 s for it

Release: AITURK IDE 1.0.0-beta.26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:04:58 +03:00
24 changed files with 2884 additions and 105 deletions
+67 -4
View File
@@ -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
not need a private local model allowlist. `/router/catalog` supplies only the
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
entries and internal 9Router combos are excluded. Combos sort A–Z first, then
individual models by provider and name, including OpenAI subscription models.
Codex labels the first visible row `isDefault`; a saved model selection remains
unchanged. A failed or empty refresh keeps the last successful catalog.
entries and internal 9Router combos are excluded. Codex's own subscription models
stay first, in their own order and under their own names; added entries follow
them: combos A–Z, then individual models by provider and name. A saved model
selection remains unchanged. A failed or empty refresh keeps the last successful catalog.
- `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made
by one provider stays readable after the user switches to another.
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
@@ -26,3 +26,66 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
`POST /aiturk/codex-baglama/stop` ends the process.
Tests: `python -m unittest discover` in this directory.
## Version 8 (IDE beta.27): changing the model by saying so
Measured on a live voice chat (11 Oct 2026): a model picked on the phone reaches this machine only with a typed
message that STARTS a turn; a message that joins a running turn (including the lone `.`) does not carry it.
`model_gecis.py` therefore handles the change in the bridge. A short user message such as "modeli gemini flash low
yap" (spoken through the voice assistant or typed) is matched against the model list; the chat's requests are then
routed to that model (`model-yonlendirme.json`, keyed by `prompt_cache_key`) until Codex sends a different model
for the chat, i.e. until the user picks another one in the list.
- Idle chat: the bridge answers itself ("Sohbetin modeli artık X ..."), which also tells the voice assistant.
- Running turn: the command is replaced by a handover note and the SAME turn continues on the new model.
- "seçili model hangisi" / "ana model kim" is answered by the bridge with the model actually in use.
- An ambiguous name changes nothing and lists the closest names; a name matching no model is not a command.
- A lone `.` that joins a running turn is dropped instead of answered (answering it cut the work short).
- Handled commands left in the history are turned into notes so a later model does not answer them again.
Setting: `sesle_model` (default on).
Audit follow-up (11 Oct 2026, same version number; `GET .../status` now also returns `kod`, the first 8 hex digits
of `bridge.py`'s SHA-256, to tell builds apart):
- Numbers said as words ("opus beş beş", "üç nokta sekiz") and a trailing "ve devam et" no longer drop the command.
- Picking the same model again in the list does not clear a spoken route (Codex sends the same name), so the
bridge's answer says how to go back. A route that is in use no longer expires after 7 days.
- A command is turned into a "model was changed" note only if it took effect; a mid-turn note also reaches a
subscription model when hook messages follow it.
- Combos: a Claude member selects the stricter token estimate; one unknown member no longer switches the overflow
guard off for a known small member; a combo dropped by the peer after the request was sent is not resent.
- An old `9r/…` name without a route gets a local 503 instead of going to chatgpt.com; upstream error bodies
reach Codex whole, with `Retry-After`.
- The last model list is also served when chatgpt.com cannot be reached at all, and is cached when the answer
was gzip-compressed. The old `display_map` setting is no longer read (subscription models keep their names).
- Log lines carry the date; the access-token tail is no longer logged.
### Work handoff instruction (prepared, off by default)
`is_hatti.py`: when `aiturk_hat` is enabled and has a `komut`, the bridge prepends a developer message ("small work
yourself; larger work goes to AITURK via this command") to requests whose `originator` header is `Codex Desktop`.
The Codex CLI (`codex-tui`, `codex_exec`) never receives it and nothing is written to any file it reads. Each new
`originator` value is logged once (`istemci goruldu`).
## Version 7 (IDE beta.26): external models behave like subscription models
Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn).
| Area | Before | Now |
| --- | --- | --- |
| Context window | every added model declared 272 000 | `pencere.py`: NanoGPT's own list for `nano/*`, a local table for `cc`/`cx`/`ag`, a combo declares its smallest member, unknown = 128 000, nothing above 272 000 (the summarizer's window). Codex then compacts by itself when switching to a smaller model. |
| Overflow | the target answered with an empty stream; Codex retried five times | `pencere.sigdir` trims the middle of oversized old parts in the outgoing copy; if it still does not fit Codex gets `context_length_exceeded`. A limit reported by the target is learned (`pencere-ogrenilen.json`). |
| Tools | clones inherited `code_mode_only` + Responses Lite; GLM and Qwen failed 0/3 on simple tasks (empty answers, reasoning leaked into the answer) | `dis_arac_modu: "direct"` (default): classic tools. GLM 3/3, Qwen 2/3; Claude, `cx` and Gemini unchanged (6/6). `"kalip"` restores the old behaviour. |
| Intermediate messages | external assistant messages had no `phase`; Codex treats those as final answers | `SseRewriter` stamps `commentary` / `final_answer`. Deltas are never delayed; only the item's `done` event waits for what follows. |
| Interrupted answer | text written before a stop or an app exit was lost | `yarim.py` keeps it (memory only) when the *client* closes the stream and adds it as a note to the next user message of the same conversation, once. |
| Images | removed from every old message for all external models | kept for vision-capable targets (user messages, newest first, 1 MB total). |
| Summary brake | the 4th summary in 10 minutes failed the turn | three summaries in a row with no normal request in between, or eight in 10 minutes. |
| Gateway errors | `event: error` (e.g. `upstream 402`) looked like a cut stream | permanent ones become `response.failed` with a readable message; short upstream bodies are logged. |
| Start-up | the model list was fetched before listening; a slow gateway made the IDE give up after 10 s | window data comes from the cache at start and is refreshed in the background; the IDE waits 45 s. |
Not changed: subscription models, their order and names, and everything on the voice path (`/realtime/*` passes through untouched).
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> seçildi." (the target is not probed, so it does not say "ready") and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
+619 -64
View File
@@ -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.
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
try:
from kanca_context import pano_blogunu_ulastir # optional, site-specific hook context
@@ -25,9 +25,18 @@ except ImportError:
from resim_context import resimleri_sadelestir
import ide_catalog
import ozet_kopru
import pencere
import yarim
import model_gecis
import is_hatti
SURUM = "6"
SURUM = "8"
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
# interpreters the matching `zstandard` build ships next to this file.
@@ -93,8 +102,13 @@ def is_timeout(error):
"""True for a socket/read timeout, also when urllib wraps it in URLError."""
reason = getattr(error, "reason", None)
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))
DISPLAY_MAP = CFG.get("display_map", {})
UA = CFG.get("user_agent", "AstraBridge/1.0 (codex)")
# 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"])
@@ -104,7 +118,7 @@ lock = threading.Lock()
seq = [0]
def log(msg):
line = time.strftime("%H:%M:%S ") + msg
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
print(line, flush=True)
with lock:
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
@@ -165,13 +179,14 @@ load_9router_catalog()
IDE_PREFIX = "ide/"
IDE_ROUTE = "aiturk-ide"
IDE_DURUM = [None]
IDE_BILGI = {} # slug -> {"pencere", "gorsel", "arac"} (pencere.coz)
def load_ide_catalog():
def load_ide_catalog(ag=True):
"""IDE'nin gecit katalogunu "ide/<id>" olarak ekler. Ayar kapaliysa ya da IDE anahtari
yoksa girdiler kaldirilir; gecit gecici olarak cevap vermezse son liste korunur."""
global MODEL_MAP, EXTRA_MODELS
try:
durum, base, anahtar, girdiler = ide_catalog.yukle(UA)
durum, base, anahtar, girdiler = ide_catalog.yukle(UA, home=HOME, ag=ag)
except Exception as e:
log(f"AITURK IDE katalogu alinamadi, son liste korunuyor: {e}")
return
@@ -195,7 +210,9 @@ def load_ide_catalog():
"description": g.get("aciklama") or f"AITURK IDE: {g['ad']}",
"route": IDE_ROUTE, "target": g["id"],
"clone_from": clone, "priority": 300 + n, "reasoning_levels": levels,
"kind": g["kind"]})
"kind": g["kind"], "context_window": g.get("pencere")})
IDE_BILGI[slug] = {"pencere": g.get("pencere"), "gorsel": bool(g.get("gorsel")), "arac": g.get("arac"),
"kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))}
model_map[slug] = (IDE_ROUTE, g["id"], None)
n += 1
else:
@@ -207,11 +224,18 @@ def load_ide_catalog():
IDE_DURUM[0] = ozet
log(f"AITURK IDE Codex Baglama: {ozet}")
load_ide_catalog()
# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler);
# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir.
load_ide_catalog(ag=False)
def catalog_refresh_loop():
"""Kataloglari arka planda yeniler; liste istegini yavaslatmaz."""
minutes = max(1, int(CFG.get("catalog_refresh_min", 5)))
time.sleep(3)
try:
load_ide_catalog() # acilistan hemen sonra: guncel pencere verisiyle
except Exception as e:
log(f"katalog yenileme hatasi: {e}")
while True:
time.sleep(minutes * 60)
for yukle in (load_9router_catalog, load_ide_catalog):
@@ -222,6 +246,23 @@ def catalog_refresh_loop():
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)
CACHE_DIR = os.path.join(LOGDIR, "son-liste")
@@ -237,7 +278,7 @@ def save_last_models(acct, hdrs, body):
yol = _cache_path(acct)
veri = {"acct": acct, "headers": hdrs, "at": time.strftime("%Y-%m-%d %H:%M:%S"),
"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:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol) # yarim dosya birakmamak icin
@@ -267,6 +308,48 @@ SLIM_CLONES = bool(CFG.get("slim_clones", False))
SLIM_FIELDS = CFG.get("slim_fields", ["model_messages", "base_instructions"])
# ---- 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):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
custom = set()
@@ -410,12 +493,134 @@ def drop_foreign_items(req):
return atilan
ARAC_OGELERI = {"function_call", "custom_tool_call", "local_shell_call", "tool_search_call", "web_search_call",
"mcp_call", "computer_call", "image_generation_call", "code_interpreter_call"}
def asistan_mesaji_mi(item):
return isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "assistant"
def phase_damgala(cikti):
"""Bitmis yanitin cikti listesinde phase'i olmayan asistan mesajlarini damgalar. Kural: yanitta arac
cagrisi varsa tur suruyor demektir, butun mesajlar ara aciklamadir; yoksa yalniz SON mesaj son cevaptir.
Codex masaustu ara mesaji bu isaretle tanir; isaretsiz her mesaji son cevap sayar (10 Ekim 2026)."""
arac_var = any(isinstance(o, dict) and o.get("type") in ARAC_OGELERI for o in cikti)
mesajlar = [o for o in cikti if asistan_mesaji_mi(o)]
for o in mesajlar:
if not o.get("phase"):
o["phase"] = "commentary" if (arac_var or o is not mesajlar[-1]) else "final_answer"
class SseRewriter:
"""function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE."""
def __init__(self, custom):
"""function_call -> custom_tool_call donusumu (custom arac adlari icin), satir satir SSE.
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 ->
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
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.buf = b""
self.custom_ids = set()
self.bekleyen = None # phase karari bekleyen "output_item.done" olayi
self.arac_goruldu = False
self.hedef = hedef # gecit model kimligi (tasma siniri ogrenmek icin)
self.tasma = tasma # callable(hedef, hata metni): sinir ogrenildiyse cagrilir
self.aracsiz = aracsiz # callable(hedef): hedef "arac cagrisi desteklenmiyor" dedi
def _birak(self, phase):
d, self.bekleyen = self.bekleyen, None
if d is not None and not d["item"].get("phase"):
d["item"]["phase"] = phase
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):
"""Bir olay -> gonderilecek olaylar listesi (bekleyen mesaj dogru phase ile one alinir)."""
t = d.get("type", "")
if t == "error" and not isinstance(d.get("error"), dict) and d.get("message"):
# Gecidin duz bicimi: {"type":"error","code":"bad_request","message":"[400]: ...","status_code":400}
d = {"type": "error", "error": {"message": str(d.get("message")), "type": str(d.get("code") or ""),
"status_code": d.get("status_code")}}
if t == "error" and isinstance(d.get("error"), dict):
cevrilen = self._hata_cevir(d)
if cevrilen is not None:
# Bekleyen mesajin bitis olayi hatadan SONRA cikmasin: once o birakilir.
once = [self._birak("commentary" if self.arac_goruldu else "final_answer")] if self.bekleyen is not None else []
return once + cevrilen
return self._sirala_normal(d, t)
def _hata_cevir(self, d):
"""Gecidin "event: error" olayi -> Codex'in tanidigi olaylar; None: gecici hata, oldugu gibi kalir."""
# Gecit kalici hatayi (kredi/yetki) "event: error" ile bildirip akisi kapatiyor; Codex bunu tanimaz,
# 5 kez yeniden baglanir ve "stream closed" der (10 Ekim 2026: "upstream 402"). Kalici olanlar Codex'in
# tanidigi bicime cevrilir; gecici olanlar oldugu gibi birakilir ki Codex yeniden denesin.
metin = str(d["error"].get("message") or d["error"].get("type") or "")
if pencere.aracsiz_mi(metin):
if self.aracsiz is not None:
self.aracsiz(self.hedef)
return self._gorunur_hata("Bu model araç çağrısını desteklemiyor; Codex'te çalışamaz. Başka bir model seçin.")
if self.dolu and pencere.kapali_hata_mi(metin):
# Pencereye yakin istekte gecidin nedensiz 400'u: buyuk olasilikla sigmadi. Codex'e sessiz kopma
# yerine anlasilir hata gider (ogrenme yapilmaz; neden kesin degil).
return [{"type": "response.failed", "response": {
"id": "resp_astrabridge_hata", "object": "response", "status": "failed", "output": [],
"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
if item and item.get("type") in ARAC_OGELERI:
self.arac_goruldu = True
once = []
if self.bekleyen is not None and (t == "response.output_item.added"
or (t == "response.output_item.done" and item is not None)):
once.append(self._birak("commentary"))
if t in ("response.completed", "response.incomplete", "response.failed") and self.bekleyen is not None:
once.append(self._birak("commentary" if self.arac_goruldu else "final_answer"))
if t == "response.completed" and isinstance((d.get("response") or {}).get("output"), list):
phase_damgala(d["response"]["output"])
if t == "response.failed" and self.tasma is not None:
hata = (d.get("response") or {}).get("error") or {}
metin = str(hata.get("message") or "")
if pencere.tasma_mi(metin) or pencere.tasma_mi(str(hata.get("code") or "")):
hata["code"] = "context_length_exceeded" # Codex'in tanidigi tasma kodu
self.tasma(self.hedef, metin)
if t == "response.output_item.done" and asistan_mesaji_mi(item) and not item.get("phase"):
if self.arac_goruldu:
item["phase"] = "commentary"
else:
self.bekleyen = d
return once
return once + [d]
def _fix(self, d):
t = d.get("type", "")
item = d.get("item")
@@ -448,6 +653,8 @@ class SseRewriter:
return d
def feed(self, chunk):
self.buf += chunk
if b"\r" in self.buf:
self.buf = self.buf.replace(b"\r\n", b"\n") # CRLF ile ayrilan SSE de cevrilir
out = b""
while b"\n\n" in self.buf:
block, self.buf = self.buf.split(b"\n\n", 1)
@@ -459,20 +666,74 @@ class SseRewriter:
d = json.loads(b"\n".join(lines[i][5:].strip() for i in data_idx))
except Exception:
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)
if d is None:
continue
ev = [l for l in lines if l.startswith(b"event:")]
evline = (b"event: " + d.get("type", "").encode() + b"\n") if ev else b""
out += evline + b"data: " + json.dumps(d, ensure_ascii=False).encode("utf-8") + b"\n\n"
for o in self._sirala(d):
evline = (b"event: " + o.get("type", "").encode() + b"\n") if ev else b""
out += evline + b"data: " + json.dumps(o, ensure_ascii=False).encode("utf-8") + b"\n\n"
return out
def flush(self):
b, self.buf = self.buf, b""
if self.bekleyen is not None:
# Akis bitis olayi gelmeden koptu: bekleyen mesaj yutulmaz, isaretsiz haliyle iletilir.
o, self.bekleyen = self.bekleyen, None
b = (b"event: " + o.get("type", "").encode() + b"\ndata: "
+ json.dumps(o, ensure_ascii=False).encode("utf-8") + b"\n\n") + b
return b
RESP_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"}
REQ_HOP = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"}
NOKTA_ISARETLERI = {".", "。"}
def nokta_mesaji_mi(req):
"""Son girdi, metni yalnizca "." olan bir kullanici mesaji mi? Kullanici sesli konusmada model degistirdikten
sonra secimin uygulanmasi icin tek bir "." yaziyor (10 Ekim 2026). Bu mesaj bir istek degildir."""
girdi = req.get("input")
if not isinstance(girdi, list) or not girdi:
return False
son = None
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
continue
son = oge
break
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return False
icerik = son.get("content")
if isinstance(icerik, str):
metin = icerik
elif isinstance(icerik, list) and all(isinstance(p, dict) and p.get("type") in ("input_text", "text") for p in icerik):
metin = "".join(str(p.get("text") or "") for p in icerik)
else:
return False # resim, dosya vb. tasiyan mesaj nokta mesaji sayilmaz
return metin.strip() in NOKTA_ISARETLERI
def hazir_yaniti(slug, metin):
"""Modele gitmeden uretilen, Codex'in tam bir yanit olarak kabul ettigi kisa akis."""
damga = "%d" % int(time.time() * 1000)
rid, mid = "resp_astrabridge_" + damga, "msg_astrabridge_" + damga
oge = {"id": mid, "type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
"content": [{"type": "output_text", "text": metin, "annotations": []}]}
kullanim = {"input_tokens": 0, "input_tokens_details": {"cached_tokens": 0}, "output_tokens": 0,
"output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 0}
olaylar = [
{"type": "response.created", "response": {"id": rid, "object": "response", "status": "in_progress",
"model": slug, "output": []}},
{"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": metin},
{"type": "response.output_item.done", "output_index": 0, "item": oge},
{"type": "response.completed", "response": {"id": rid, "object": "response", "status": "completed",
"model": slug, "output": [oge], "usage": kullanim}},
]
return b"".join(b"event: " + o["type"].encode() + b"\ndata: " + json.dumps(o, ensure_ascii=False).encode("utf-8")
+ b"\n\n" for o in olaylar)
def claude_sonu_duzelt(req):
"""9Router Claude cevirisi developer/system mesajlarini dusurur; geriye kalan son oge asistan mesaji ya da
reasoning ise Claude 'assistant message prefill' hatasi (400) verir. O durumda sona kisa user mesaji eklenir.
@@ -508,6 +769,54 @@ def decompress(body, enc):
return body
OZET_FRENI = ozet_kopru.DonguFreni()
YONLENDIRME = model_gecis.Yonlendirme(HOME)
GORULEN_ISTEMCI = set() # originator degerleri; her biri gunluge bir kez yazilir
_BILINEN = {"kaynak": None, "liste": []}
def bilinen_modeller():
"""Codex'e en son verilen listedeki (slug, gorunen ad) ciftleri: abonelik modelleri + eklenenler."""
son = max(LAST_MODELS.values(), key=lambda v: str(v.get("at") or ""), default=None) if LAST_MODELS else None
govde = son.get("body") if son else None
if govde is not None and _BILINEN["kaynak"] is not govde:
try:
liste = [(m["slug"], m.get("display_name") or m["slug"]) for m in json.loads(govde).get("models", [])
if isinstance(m, dict) and m.get("slug")]
except (ValueError, AttributeError, TypeError):
liste = []
_BILINEN["kaynak"], _BILINEN["liste"] = govde, liste
liste = list(_BILINEN["liste"])
gorulen = {slug for slug, _ in liste}
liste += [(m["slug"], m.get("display_name") or m["slug"]) for m in EXTRA_MODELS
if m.get("slug") and m["slug"] not in gorulen]
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):
return next((ad for kimlik, ad in bilinen_modeller() if kimlik == slug), None) or slug
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@@ -517,7 +826,10 @@ class H(http.server.BaseHTTPRequestHandler):
if "chunked" in (self.headers.get("Transfer-Encoding") or "").lower():
out = bytearray()
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:
continue
size = int(line.split(b";")[0], 16)
@@ -531,7 +843,7 @@ class H(http.server.BaseHTTPRequestHandler):
n = int(self.headers.get("Content-Length") or 0)
return self.rfile.read(n) if n else b""
def _stream(self, resp, status, tag, rewriter=None):
def _stream(self, resp, status, tag, rewriter=None, izle=None):
self.send_response(status)
for k, v in resp.headers.items():
if k.lower() in RESP_HOP: continue
@@ -539,20 +851,61 @@ class H(http.server.BaseHTTPRequestHandler):
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
total = 0
bas = b"" # ust kaynagin ilk baytlari: cok kisa (bos) akislarin nedeni gunluge yazilir
def w(b):
if b:
self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush()
try:
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
total += len(chunk)
if len(bas) < 600:
bas += chunk[:600 - len(bas)]
if izle is not None:
izle.feed(chunk)
w(rewriter.feed(chunk) if rewriter else chunk)
if rewriter: w(rewriter.flush())
self.wfile.write(b"0\r\n\r\n"); self.wfile.flush()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
log(f"{tag} istemci baglantiyi kapatti ({total} bayt)")
# Yanit bitmeden istemci kapatti (durdurma ya da uygulama kapanmasi): yarim metin sonraki tura tasinir.
if izle is not None and yarim.kaydet(izle):
log(f"{tag} yarim cevap saklandi ({len(izle.metin)} karakter); ayni sohbetin sonraki istegine not olarak eklenecek")
log(f"{tag} bitti status={status} {total} bayt")
if rewriter is not None and total < 600:
log(f"{tag} kisa akis govdesi: {bas[:500]!r}")
def _tasma_ogren(self, hedef, metin):
sinir = pencere.sinir_oku(metin)
if sinir and pencere.ogren(HOME, hedef, sinir):
log(f"pencere ogrenildi: {hedef} gercek siniri {sinir} token; model listesi yenileniyor")
threading.Thread(target=load_ide_catalog, daemon=True).start()
def _aracsiz_ogren(self, hedef):
if pencere.ogren_aracsiz(HOME, hedef):
log(f"aracsiz model ogrenildi: {hedef}; model listesinde isaretlenecek")
threading.Thread(target=load_ide_catalog, daemon=True).start()
def _send_sse_failed(self, kod, mesaj):
"""Codex'in akis icinde tanidigi hata bicimi (response.failed + error.code)."""
kimlik = "resp_astrabridge_%d" % int(time.time() * 1000)
olay = {"type": "response.failed", "response": {"id": kimlik, "object": "response", "status": "failed",
"error": {"code": kod, "message": mesaj}, "output": []}}
govde = b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n"
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
def _send_json(self, status, obj):
b = json.dumps(obj).encode()
@@ -575,9 +928,10 @@ class H(http.server.BaseHTTPRequestHandler):
self.command = "GET"
if self.command == "GET" and path.rstrip("/").endswith("/status"):
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]),
"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"):
self._send_json(200, {"ok": True})
log("AITURK IDE istegiyle kapatiliyor")
@@ -601,11 +955,11 @@ class H(http.server.BaseHTTPRequestHandler):
return self._control(path)
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)}")
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._passthrough(n, path, body)
def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None):
def _passthrough(self, n, path, body, tagname="pass", rewritten=False, rewriter=None, izle=None):
tag = f"[{n} {tagname} {self.command} {path[:80]}]"
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP}
if rewritten:
@@ -619,23 +973,19 @@ class H(http.server.BaseHTTPRequestHandler):
except urllib.error.HTTPError as e:
resp = e; status = e.code
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:
acct = self.headers.get("chatgpt-account-id") or "-"
tok = (self.headers.get("Authorization") or "")[-8:]
if status == 200:
out = self._models(resp, tag)
return out
body = resp.read()[:300]
log(f"{tag} chatgpt.com {status} verdi (hesap={acct} token=..{tok}) govde={body[:160]!r}")
cached = LAST_MODELS.get(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']})")
log(f"{tag} chatgpt.com {status} verdi (hesap={acct}) govde={body[:160]!r}")
if self._son_listeyi_ver(tag, acct):
return
self.send_response(status)
self.send_header("Content-Type", "application/json"); self.send_header("Content-Length", str(len(body)))
@@ -658,7 +1008,20 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} sesli on hazirlayici baslatildi")
except Exception as e:
log(f"{tag} sesli on hazirlayici baslatilamadi: {type(e).__name__}")
self._stream(resp, status, tag, rewriter)
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):
"""OpenAI'nin sifreli ozetini bir kez OpenAI'ye duz metne dokturur; sonucu diske onbellekler (icerik gunluge yazilmaz)."""
@@ -725,12 +1088,11 @@ class H(http.server.BaseHTTPRequestHandler):
data = json.loads(raw)
models = data.get("models", [])
changed = 0
for m in models:
new = DISPLAY_MAP.get(m.get("slug"))
if new:
m["display_name"] = new; changed += 1
# Ozgun abonelik modellerinin adi, sirasi ve yeri degistirilmez (eski "display_map" ayari artik okunmaz).
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:
continue
base = by_slug.get(x.get("clone_from") or "gpt-6-astra") or (models[0] if models else {})
@@ -741,7 +1103,18 @@ class H(http.server.BaseHTTPRequestHandler):
m["visibility"] = "list"
m["priority"] = x.get("priority", 0)
m["upgrade"] = None; m["availability_nux"] = None
m["additional_speed_tiers"] = []; m["service_tiers"] = []
dis = x["slug"].startswith(IDE_PREFIX)
if not dis:
m["additional_speed_tiers"] = []; m["service_tiers"] = []
# Dis modelde hiz katmani ilan edilmeye devam eder (yoksa Codex her turda "service tier ... will be
# omitted" uyarisi basar); istekteki service_tier alani gecide gonderilmeden once silinir.
if dis and CFG.get("dis_arac_modu", "direct") == "direct":
# Abonelik kalibi "code_mode_only": tek arac JavaScript calistiran `exec`. Zayif modeller bunu
# cagiramiyor ya da ciktiyi yazdirmiyor (10 Ekim 2026 olcumu). Dis modellere Codex'in klasik
# araclari dogrudan verilir (GPT-5.5 kalibindaki gibi); Responses Lite kapatilir.
m["tool_mode"] = None
m["use_responses_lite"] = False
m["experimental_supported_tools"] = []
if x.get("context_window"):
cw = int(x["context_window"])
m["context_window"] = cw; m["max_context_window"] = cw
@@ -759,13 +1132,14 @@ class H(http.server.BaseHTTPRequestHandler):
if not m.get("base_instructions") and not has_tmpl:
m.update(saved)
models.append(m); changed += 1
if EXTRA_MODELS:
ide_catalog.sirala_codex(models, EXTRA_MODELS)
if ekler:
ide_catalog.sirala_codex(models, ekler, ozgun_kimlikler)
out = json.dumps(data, ensure_ascii=False).encode("utf-8")
except Exception as e:
log(f"{tag} model listesi degistirilemedi: {e}")
out = raw; changed = 0
raw_len = len(out)
duz = out # sikistirilmamis govde: yedege bu yazilir
catalog_etag = 'W/"astrabridge-' + hashlib.sha256(out).hexdigest()[:24] + '"'
enc_out = None
if "gzip" in (self.headers.get("Accept-Encoding") or "").lower() and changed:
@@ -778,15 +1152,16 @@ class H(http.server.BaseHTTPRequestHandler):
if kl in ("etag", "x-models-etag") and changed:
v = catalog_etag
hdrs_out.append((k, v))
if enc_out:
hdrs_out.append(("Content-Encoding", enc_out))
if changed and not enc_out:
if changed:
# Yedek her zaman sikistirilmamis govdeyle tutulur (istemci gzip istese de).
acct_ = self.headers.get("chatgpt-account-id") or "-"
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.
if not onceki or onceki.get("body") != out:
save_last_models(acct_, hdrs_out, out)
if not onceki or onceki.get("body") != duz:
save_last_models(acct_, list(hdrs_out), duz)
if enc_out:
hdrs_out.append(("Content-Encoding", enc_out))
self.send_response(200)
for k, v in hdrs_out:
self.send_header(k, v)
@@ -795,6 +1170,72 @@ class H(http.server.BaseHTTPRequestHandler):
self.wfile.write(out)
log(f"{tag} bitti status=200 {raw_len} bayt (gonderilen {len(out)}, {enc_out or 'sikistirmasiz'}), {changed} model girdisi degistirildi/eklendi")
def _yerel_yanit(self, slug, metin):
govde = hazir_yaniti(slug, metin)
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(govde)))
self.end_headers()
self.wfile.write(govde)
def _model_komutu(self, n, req, sohbet, gelen):
"""Son kullanici mesaji "modeli X yap" / "hangi model" ise isler. Yerel cevap metni doner;
None: istek (gerekirse degistirilmis olarak) modele gider."""
metin, sira, sesli = model_gecis.son_kullanici_metni(req)
komut = model_gecis.komut_coz(metin) if metin else None
if not komut:
return None
etkin = YONLENDIRME.al(sohbet, gelen) or gelen
ortada = model_gecis.tur_ortasi(req, sira)
if komut["tur"] == "sor":
if ortada:
return None
log(f"[{n} {gelen}] model sorusu kopruden cevaplandi: {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())
if not sonuc:
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:
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?")
log(f"[{n} {gelen}] model komutu belirsiz: {komut['ad']} -> {sonuc['adaylar']}")
if not ortada:
return soru
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 + "]")
self.komut_govdeyi_degistirdi = True
return None
hedef = sonuc["slug"]
if hedef.startswith("ide/") and MODEL_MAP.get(hedef) is None and "aiturk-ide" not in ROUTES:
return "AITURK bağlantısı şu an kapalı; bu modele geçemedim, model değişmedi."
YONLENDIRME.koy(sohbet, gelen, hedef)
log(f"[{n} {gelen}] model komutu: {model_adi(etkin)} -> {sonuc['ad']}"
f" ({'tur ortasi, is devrediliyor' if ortada else 'bosta'}{', sesli' if sesli else ''})")
if hedef == etkin:
return None if ortada else f"Sohbetin modeli zaten {sonuc['ad']}."
if ortada:
model_gecis.metni_degistir(req, sira, model_gecis.devir_notu(sonuc["ad"], model_adi(etkin)))
self.komut_govdeyi_degistirdi = True
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. "
f"Bundan sonraki işleri {sonuc['ad']} yapacak; hangi model diye sorulursa bunu söyle.{donus}")
def _responses(self, n, path, body):
try:
req = json.loads(decompress(body, self.headers.get("Content-Encoding")))
@@ -802,27 +1243,105 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} responses] govde cozulemedi: {e}")
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
slug = req.get("model", "")
gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model)
govde_degisti = False # istek govdesi kopruce degistirildi: abonelik yolunda yeniden yazilir
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 "")
if CFG.get("sesle_model", True) and sohbet:
self.komut_govdeyi_degistirdi = False
try:
yerel = self._model_komutu(n, req, sohbet, gelen)
except Exception as e:
yerel = None
log(f"[{n} {gelen}] model komutu atlandi: {type(e).__name__}: {e}")
if yerel is not None:
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:
if model_gecis.gecmisi_temizle(req, bilinen_modeller(), YONLENDIRME.al(sohbet, gelen) or gelen):
govde_degisti = True
except Exception as e:
log(f"[{n} {gelen}] gecmis komut temizligi atlandi: {type(e).__name__}: {e}")
hedef = YONLENDIRME.al(sohbet, gelen)
if hedef and hedef != gelen:
req["model"] = slug = hedef
log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor")
mapping = MODEL_MAP.get(slug)
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
# keser. Mesaj cikarilir, model isine devam eder; kullanici model bosalinca yeniden yazar.
del req["input"][model_gecis.son_sira(req["input"])] # yalniz nokta; ardindaki kanca mesajlari kalir
govde_degisti = True
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)):
govde_degisti = True
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req):
# 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.
log(f"[{n} {slug}] nokta mesaji kopruden karsilandi (modele gonderilmedi)")
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.
yarim_eklenen = 0
if CFG.get("yarim_cevap", True):
try:
yarim_eklenen = yarim.uygula(req, sohbet)
except Exception as e:
log(f"[{n} {slug}] yarim cevap notu atlandi: {type(e).__name__}: {e}")
if yarim_eklenen:
log(f"[{n} {slug}] yarim cevap notu eklendi ({yarim_eklenen} karakter)")
izle = yarim.Izleyici(sohbet, slug) if (sohbet and CFG.get("yarim_cevap", True)) else None
try:
if not ozet_kopru.tetik_mi(req):
OZET_FRENI.normal(sohbet or slug) # ozet olmayan istek: art arda ozet sayaci sifirlanir
except Exception:
pass
ozet_cfg = CFG.get("ozet_kopru") or {}
ozet_acik = ozet_cfg.get("enabled", False)
catalog = CFG.get("nine_router_catalog") or {}
prefix = catalog.get("prefix", "9r/")
if mapping is None and catalog.get("enabled") and slug.startswith(prefix):
# Newly added API models can be selected before the background refresh.
# Provider-qualified IDs are passed intact to 9Router, never to ChatGPT.
mapping = (catalog.get("route", "9router"), slug[len(prefix):], None)
if mapping is None and prefix and slug.startswith(prefix):
if catalog.get("enabled") and catalog.get("route", "9router") in ROUTES:
# Newly added API models can be selected before the background refresh.
# 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
if mapping is None and slug.startswith(ide_prefix) and ide_route in ROUTES:
mapping = (ide_route, slug[len(ide_prefix):], None)
if mapping is None and slug.startswith(ide_prefix):
# Never send an IDE model to chatgpt.com under another name.
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."}})
if mapping is None:
# abonelik modeli: chatgpt.com'a oldugu gibi
fixed = fix_tool_ids(req)
atilan = drop_foreign_items(req)
degisti = False
degisti = bool(yarim_eklenen) or govde_degisti
ozet_akisi = None
if ozet_acik:
# Ozet bagdastirici (ozet_kopru.py; 9 Ekim 2026'da yalniz bu parca geri acildi): koprunun urettigi
@@ -832,7 +1351,7 @@ class H(http.server.BaseHTTPRequestHandler):
# kapsam "hepsi": OpenAI modelinde de ozeti kopru DUZ METIN urettirir; boylece ozet sonradan
# secilen her saglayicida okunur. kapsam "9router": OpenAI ozeti yerlesik (sifreli) kalir.
if ozet_cfg.get("kapsam", "hepsi") == "hepsi" and ozet_kopru.tetik_mi(req):
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"[{n} CHATGPT {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
@@ -860,7 +1379,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} CHATGPT {slug}] ozet kopru: {ozet_akisi.sonuc_notu()}")
return sonuc
return self._passthrough(n, path, body, tagname=f"CHATGPT {slug}",
rewritten=bool(fixed or atilan or degisti))
rewritten=bool(fixed or atilan or degisti), izle=izle)
route_name, target, flat = mapping
try:
ozet_tetik = ozet_acik and ozet_kopru.tetik_mi(req)
@@ -869,7 +1388,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} {slug}] ozet kopru atlandi: {type(e).__name__}: {e}")
girdi_bayt = 0
if ozet_tetik:
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 3)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600))
OZET_FRENI.sinir = int(ozet_cfg.get("dongu_siniri", 8)); OZET_FRENI.pencere = int(ozet_cfg.get("dongu_pencere_sn", 600)); OZET_FRENI.art_arda = int(ozet_cfg.get("dongu_art_arda", 3))
if not OZET_FRENI.izin(str(req.get("prompt_cache_key") or slug), time.time()):
log(f"[{n} {slug}] ozet kopru: DONGU FRENI, ozet istegi reddedildi (ayni sohbette {OZET_FRENI.sinir} ozetten sonra)")
return self._send_json(400, {"error": {"message": ozet_kopru.DONGU_MESAJI % OZET_FRENI.sinir,
@@ -888,10 +1407,16 @@ class H(http.server.BaseHTTPRequestHandler):
return
# 9Router cevirisinden gecen hedefler (9Router ve AITURK IDE gecidi): ayni istek duzeltmeleri uygulanir.
via_router = route_name in (catalog.get("route", "9router"), ide_route)
route = ROUTES[route_name]
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}]"
req["model"] = target
req.pop("client_metadata", None)
if route_name == ide_route:
req.pop("service_tier", None) # abonelik hiz katmani; gecit tanimaz
for k in route.get("strip_fields", []):
req.pop(k, None)
if ozet_acik:
@@ -901,16 +1426,20 @@ class H(http.server.BaseHTTPRequestHandler):
if ozet_tetik:
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)")
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).
# 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")))
if cikan:
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)
rewriter = SseRewriter(custom)
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 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).
cikan, kazanc = resimleri_sadelestir(req)
if cikan:
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
merged = coalesce_context(req)
if merged:
@@ -926,6 +1455,21 @@ class H(http.server.BaseHTTPRequestHandler):
eklendi, sonlar = claude_sonu_duzelt(req)
if eklendi:
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
# 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:
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:
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
if not sigdi:
log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})")
return self._send_sse_failed("context_length_exceeded",
f"Bu mesaj seçili modelin bağlam penceresine sığmıyor (yaklaşık {tahmin} token, pencere {hedef_pencere}). "
"Daha büyük pencereli bir model seçin ya da mesajı kısaltın.")
data = json.dumps(req, ensure_ascii=False).encode("utf-8")
log(f"{tag} girdi={len(req.get('input', []))} oge, arac={len(req.get('tools', []) or [])} {len(data)} bayt")
if DEBUG:
@@ -941,8 +1485,8 @@ class H(http.server.BaseHTTPRequestHandler):
try:
resp = urllib.request.urlopen(r, timeout=UPSTREAM_IDLE_TIMEOUT)
except urllib.error.HTTPError as e:
errbody = e.read()[:400]
last = (e.code, errbody)
errbody = e.read() # istemciye TAM govde gider (400 bayta kesilince JSON bozuluyordu); gunlukte kisaltilir
last = (e.code, errbody[:400])
etxt = errbody.decode("utf-8", "replace")
# 9Router kalici ust-kaynak hatalarini (400/403 ...) 503 govdesine sararak
# donduruyor. Bunlari tekrar denemek dakikalarca bosuna bekletiyordu.
@@ -956,8 +1500,16 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
time.sleep(min(2 * attempt, 8)); continue
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
if pencere.tasma_mi(etxt):
# Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin
# bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur).
self._tasma_ogren(target, etxt)
return self._send_sse_failed("context_length_exceeded", etxt[:300])
self.send_response(e.code)
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.end_headers(); self.wfile.write(errbody); return
except Exception as e:
@@ -965,6 +1517,9 @@ class H(http.server.BaseHTTPRequestHandler):
if combo and is_timeout(e):
log(f"{tag} combo zaman asimi, tekrar gonderilmiyor (calisiyor olabilir): {e}")
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}")
time.sleep(min(2 * attempt, 8)); continue
# Headers go to the client from here on: a cut stream is never resent.
@@ -973,7 +1528,7 @@ class H(http.server.BaseHTTPRequestHandler):
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
return self._stream(resp, resp.status, tag, rewriter)
return self._stream(resp, resp.status, tag, rewriter, izle)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
do_POST = _handle
+37 -11
View File
@@ -11,6 +11,8 @@ Yerel config'teki model adlari uye katalogunu sinirlamaz.
"""
import json, os, re, urllib.request
import pencere
# Agent home: HERMES_HOME when the IDE passes it, else the default install location.
AGENT = os.environ.get("HERMES_HOME") or os.path.join(os.environ.get("LOCALAPPDATA", ""), "TurkServis", "AITURK-IDE", "agent")
IDE_DIR = os.path.dirname(os.path.normpath(AGENT))
@@ -146,7 +148,11 @@ def adlandir(girdiler, dolu=(), turler=None):
for k, _ in girdiler:
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
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())
sonuc[k] = ad
return sonuc
@@ -172,7 +178,8 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
if kind == "combo" and mid.lower().startswith("9router-"):
continue
gorulen.add(mid)
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0})
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0,
"yol": str(m.get("description") or "").split(" · ")[-1].strip()})
secilen.sort(key=lambda s: (s["sira"], s["id"]))
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
@@ -181,16 +188,18 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
adlar.add(taban.split(" · ")[-1].strip().lower())
s["ad"] = taban
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"]} for s in secilen]
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,
listenin basinda durur. Ek modeller onlarin ARKASINA eklenir: combo A-Z, sonra
saglayici A-Z / model A-Z. Ozgun bir modelle ayni adi tasiyan ek model " (AITURK)"
ekiyle ayirt edilir; secicide ozgunun yerine gecmis gibi gorunmez.
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz."""
bilgiler = {m["slug"]: m for m in ekler}
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz.
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]
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
@@ -230,6 +239,7 @@ def sirala_codex(modeller, ekler):
# Telefonda bu satir tek satirdir ve yaklasik 30 karakterde kesilir (kullanici ekran goruntusu,
# 10 Ekim 2026); en ayirt edici bilgi basa yazilir, ad tekrar edilmez.
ACIKLAMA_SINIRI = 30
KUCUK_PENCERE = 100_000 # bunun altindaki pencere alt satirda yazilir
_AILE = (("opus", "opus"), ("sonnet", "sonnet"), ("haiku", "haiku"), ("astra", "astra"), ("terra", "terra"),
("luna", "luna"), ("sol", "sol"), ("gemini", "gemini"), ("qwen", "qwen"), ("glm", "glm"),
("abliterated", "abliterated"), ("deepseek", "deepseek"), ("llama", "llama"), ("gemma", "gemma"))
@@ -273,9 +283,11 @@ def _sigdir(onek, adlar, ayrac):
return onek.rstrip(": ")
def aciklama(kimlik, ad, tur, kayit=None):
def aciklama(kimlik, ad, tur, kayit=None, bilgi=None):
"""Model adinin altinda gorunecek kisa satir. kayit: gecidin /router/catalog girdisi (yoksa None).
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri."""
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri.
bilgi (pencere.coz sonucu): Codex'te isi etkileyen iki uyari one yazilir: kucuk baglam ("32K")
ve arac cagiramayan model ("araçsız")."""
if tur == "combo":
topoloji = (kayit or {}).get("topology") or {}
uyeler = topoloji.get("members") or []
@@ -292,18 +304,25 @@ def aciklama(kimlik, ad, tur, kayit=None):
kisa = gorunen_ad(kimlik, ad, "model")
kod = kisa.split("/")[0].lower() if "/" in kisa else (kimlik or "").split("-")[0].lower()
parcalar = [_SAGLAYICI.get(kod, kod) or "Tek model"]
if bilgi and bilgi.get("pencere") and bilgi["pencere"] < KUCUK_PENCERE:
parcalar.append(pencere.kisa(bilgi["pencere"]))
if bilgi and bilgi.get("arac") is False:
parcalar.append("araçsız")
if _SANSURSUZ.search(kisa) or _SANSURSUZ.search(kimlik or ""):
parcalar.append("sansürsüz")
if "thinking" in (kisa + " " + (kimlik or "")).lower():
parcalar.append("düşünen")
if len(parcalar) == 1:
parcalar.insert(0, "Tek model")
while len(parcalar) > 1 and len(" · ".join(parcalar)) > ACIKLAMA_SINIRI:
parcalar.pop() # sona yazilan en az onemli ozellik duser; kelime ortasindan kesilmez
return " · ".join(parcalar)[:ACIKLAMA_SINIRI]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
Ag hatasinda istisna atar; cagiran son listeyi korur."""
Ag hatasinda istisna atar; cagiran son listeyi korur. home: koprunun veri klasoru (pencere onbellegi).
Her girdi "pencere" (ilan edilecek baglam penceresi), "gorsel" ve "arac" alanlarini tasir."""
ayar = ayar_oku()
if not ayar["enabled"]:
return "kapali", "", "", []
@@ -328,6 +347,13 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
ogr = pencere.ogrenilen(home)
aracsiz = pencere.aracsizlar(home)
for g in girdiler:
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]))
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
bilgi["arac"], bilgi["kaynak"])
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]), bilgi)
return "tamam", base, anahtar, girdiler
@@ -0,0 +1,102 @@
"""Codex IDE'ye "is kucuk degilse AITURK yapacak" talimati (11 Ekim 2026).
Kullanici karari: Codex Baglama acikken talimat Codex IDE'nin (masaustu uygulamasi) gordugu sistem talimatinda durur;
Codex CLI talimati hic gormez, varligindan haberi olmaz. Karari secili model verir; kucuk modellerin yanilmasi kabul
edilir, ayri bir yazilimla onune gecilmez.
Bu yuzden talimat hicbir dosyaya yazilmaz (AGENTS.md'yi CLI da okur): kopru, yalniz masaustu uygulamasindan gelen
isteklere, istek gecerken ekler. Istemci `originator` basligindan ayirt edilir (olcum 11 Ekim 2026: masaustu
"Codex Desktop", etkilesimli CLI "codex-tui", `codex exec` "codex_exec").
Hat, Codex Baglama'nin parcasidir: ayri bir dugmesi yoktur, bu bilgisayarda isi gonderebilen bir AITURK komutu
(`aiturk is`) bulundugu surece aciktir. Komut yoksa talimat eklenmez: gonderme yolu olmayan bir "AITURK'e gonder"
talimati modeli yaniltir. Ayar (bridge.json, istege bagli): "aiturk_hat": {"enabled": false} kapatir;
{"komut": "..."} komutu elle verir; {"talimat": "..."} metni degistirir.
"""
import os
import shutil
import subprocess
MASAUSTU = {"codex desktop"}
ISARET = "[AITURK iş hattı]"
VARSAYILAN = (
ISARET + " Bu Codex, AITURK'e bağlı. Şu kurala göre çalış:\n"
"1. Basit soruyu, bilgi isteğini ya da tek küçük düzeltmeyi kendin yap.\n"
"2. Kullanıcı \"sen yap\" derse işi kendin yap.\n"
"3. İş küçük değilse (birden çok dosya, birkaç adım, yeni özellik, araştırma gerektiren iş) kendin yapma. "
"İşin tam metnini (kullanıcının isteği ve gereken bağlam) UTF-8 bir dosyaya yaz, işi şu komutla AITURK'e gönder, "
"bitmesini bekle ve sonucu kullanıcıya kısaca aktar:\n{komut}\n"
"Komut \"işi sen yap\" derse ya da hata verirse işi kendin yap. Emin değilsen işi küçük say ve kendin yap."
)
def masaustu_mu(originator):
return str(originator or "").strip().lower() in MASAUSTU
def adaylar(ortam=None):
"""Isi gonderebilecek AITURK komutu adaylari, oncelik sirasiyla (kurulu AITURK, PATH, hat deneme kurulumu)."""
ortam = os.environ if ortam is None else ortam
yerel = ortam.get("LOCALAPPDATA") or ""
liste = []
if yerel:
liste.append(os.path.join(yerel, "TurkServis", "AITURK", "program", "bin", "aiturk.cmd"))
bulunan = shutil.which("aiturk")
if bulunan:
liste.append(bulunan)
if yerel:
liste.append(os.path.join(yerel, "TurkServis", "AITURK-is-hatti", "Scripts", "aiturk.exe"))
return [y for i, y in enumerate(liste) if y and y not in liste[:i]]
def destekliyor(yol, calistir=subprocess.run):
"""Bu AITURK `is` komutunu taniyor mu? (Eski surumler tanimaz: "No such command 'is'.")"""
if not os.path.isfile(yol):
return False
try:
sonuc = calistir([yol, "is", "--help"], capture_output=True, timeout=40, cwd=os.path.dirname(yol),
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
return sonuc.returncode == 0
except Exception:
return False
def komut_bul(ortam=None, destek=destekliyor):
"""Isi gonderebilen ilk AITURK komutunun yolu; yoksa None."""
for yol in adaylar(ortam):
if destek(yol):
return yol
return None
def komut_satiri(yol, sohbet=""):
"""Modelin calistiracagi hazir satir. Sohbet kimligi kopruden gelir (sohbetler karismasin); klasor verilmez:
komut Codex'in o anki klasorunde calisir, AITURK de isi ayni klasorde yapar."""
sohbet = "".join(c for c in str(sohbet or "") if c.isalnum() or c in "-_")
return f'"{yol}" is' + (f" --sohbet {sohbet}" if sohbet else "") + ' --dosya "<iş metnini yazdığın dosya>"'
def talimat(ayar, bulunan=None, sohbet=""):
"""Eklenecek metin; hat kapaliysa ya da gonderme komutu yoksa None.
bulunan: bu bilgisayarda bulunan AITURK komutunun yolu (komut_bul); ayardaki "komut" onun onune gecer."""
ayar = ayar if isinstance(ayar, dict) else {}
if ayar.get("enabled") is False:
return None
komut = str(ayar.get("komut") or "").strip() or (komut_satiri(bulunan, sohbet) if bulunan else "")
if not komut:
return None
return str(ayar.get("talimat") or VARSAYILAN).replace("{komut}", komut)
def ekle(req, metin):
"""Talimati istegin basina developer mesaji olarak koyar. True: eklendi (zaten varsa eklenmez)."""
girdi = req.get("input")
if not metin or not isinstance(girdi, list):
return False
for oge in girdi[:3]:
icerik = oge.get("content") if isinstance(oge, dict) else None
yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
if ISARET in yazi:
return False
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": metin}]})
return True
@@ -0,0 +1,377 @@
"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026).
Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla
ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu.
Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik
koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir),
kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir.
- Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ...").
- Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer;
yarim kalan is kesilmez, yeni model kaldigi yerden devam eder.
- Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar.
Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur.
"""
import json
import os
import re
import threading
import time
DOSYA = "model-yonlendirme.json"
OMUR_SN = 7 * 24 * 3600
YENILE_SN = 24 * 3600
EN_COK_KAYIT = 200
EN_COK_KELIME = 14
_KILIT = threading.Lock()
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
# Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir.
FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim",
"degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim",
"cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"}
OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme")
# "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir.
SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana",
"modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"}
SORU = ("hangi", "hangisi", "kim", "kimdir")
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
"bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex",
"ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona", "nokta"}
# Yaziyla soylenen surum rakamlari ("opus bes bes", "uc nokta sekiz"). "bir" belirtec de olabildigi icin
# ("bir model sec") yalniz baska bir rakamin ya da "nokta"nin yaninda rakam sayilir.
SAYI = {"sifir": "0", "bir": "1", "iki": "2", "uc": "3", "dort": "4", "bes": "5", "alti": "6", "yedi": "7",
"sekiz": "8", "dokuz": "9"}
# Komutun ardindan gelen ve is tasimayan ek ("... yap ve devam et"): komutu dusurmez, atilir.
DEVAM_EKI = {"ve", "sonra", "devam", "et", "edelim", "edin", "lutfen"}
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
"kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex",
"dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku",
"sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking",
"ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"}
# Listede kisa yazilan saglayici -> konusurken soylenen ad
SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")}
def duz(metin):
return str(metin or "").lower().translate(_TR)
def parcala(metin):
"""Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low."""
return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))]
def son_sira(girdi):
"""Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar
kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1."""
if not isinstance(girdi, list):
return -1
for i in range(len(girdi) - 1, -1, -1):
oge = girdi[i]
if isinstance(oge, dict) and oge.get("type", "message") == "message":
if oge.get("role") == "user":
return i
if oge.get("role") in ("developer", "system"):
continue
return -1
return -1
def son_kullanici_metni(req):
"""(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False).
Sesli aktarimda (<realtime_delegation>) yalnizca <input> icindeki soz alinir."""
girdi = req.get("input")
sira = son_sira(girdi)
if sira < 0:
return None, -1, False
son = girdi[sira]
icerik = son.get("content")
if isinstance(icerik, str):
metin = icerik
elif isinstance(icerik, list) and icerik and all(isinstance(p, dict) and p.get("type") in ("input_text", "text")
for p in icerik):
metin = "".join(str(p.get("text") or "") for p in icerik)
else:
return None, -1, False
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
if m:
return m.group(1).strip(), sira, True
if "<" in metin and ">" in metin:
return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz
return metin.strip(), sira, False
def _sayilari_cevir(kelimeler):
cikti = []
for i, k in enumerate(kelimeler):
if k in SAYI:
komsu = [kelimeler[j] for j in (i - 1, i + 1) if 0 <= j < len(kelimeler)]
if k != "bir" or any(x == "nokta" or x.isdigit() or (x in SAYI and x != "bir") for x in komsu):
cikti.append(SAYI[k])
continue
cikti.append(k)
return cikti
DOLGU = {"evet", "tamam", "hadi", "peki", "tabii", "hayir", "yani", "sey", "ee", "eee", "hmm", "olur", "simdi"}
def _harfleri_birlestir(kelimeler):
"""Harf harf soylenen ad tek sozcuk olur: "s e e d iki nokta bir turbo" -> "seed iki nokta bir turbo"."""
cikti, harfler = [], []
for k in kelimeler + [""]:
if len(k) == 1 and k.isalpha():
harfler.append(k)
continue
if len(harfler) >= 3:
cikti.append("".join(harfler))
else:
cikti.extend(harfler)
harfler = []
if k:
cikti.append(k)
# Ad hem heceleyerek hem duz soylendiyse ("seed s e e d ...") tekrar atilir.
return [k for i, k in enumerate(cikti) if i == 0 or k != cikti[i - 1]]
def komut_coz(metin):
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
kelimeler = _harfleri_birlestir(re.findall(r"[a-z0-9]+", duz(metin)))
if not kelimeler or len(kelimeler) > EN_COK_KELIME:
return None
if not any(k.startswith("model") for k in kelimeler):
# "modeli" denmeden soylenen komut ("Seed iki nokta bir turbo yap", "gemini flash low'a gec"): yalniz cumle
# bir komut fiiliyle bitiyorsa ve ad bir modele TAM uyuyorsa komut sayilir (kesin=False: cagiran, ad
# bulunamazsa mesaji komut saymaz, modele birakir).
if any(k.startswith(OLUMSUZ) for k in kelimeler) or kelimeler[-1] not in FIILLER or len(kelimeler) > 9:
return None
kelimeler = _sayilari_cevir(kelimeler)
ad = [TAKMA.get(k, k) for k in kelimeler[:-1] if k not in ATLA and k not in DOLGU]
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
return {"tur": "gec", "ad": ad, "kesin": False} if len(ad) >= 2 else None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
if any(k.startswith(OLUMSUZ) for k in kelimeler):
return None
fiil = [k for k in kelimeler if k in FIILLER]
if fiil:
son_fiil = max(i for i, k in enumerate(kelimeler) if k in FIILLER)
if all(k in DEVAM_EKI for k in kelimeler[son_fiil + 1:]):
kelimeler = kelimeler[:son_fiil + 1]
kelimeler = _sayilari_cevir(kelimeler)
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil and k not in DOLGU]
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
# Ad soylenmemis ("modeli değiştir", "başka modele geç"): degisiklik yok, nasil soylenecegi anlatilir.
return {"tur": "gec", "ad": ad, "kesin": True} if ad else {"tur": "nasil"}
return None
def _uyar(sorgu, parca):
"""Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar."""
if sorgu == parca:
return True
return len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit()
def eslestir(ad, modeller):
"""ad: sorgu parcalari; modeller: [(slug, gorunen ad)].
Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor)."""
if not ad:
return None
puanli = []
for slug, gorunen in modeller:
parcalar = parcala(gorunen) or parcala(slug)
kume = set(parcalar) | set(parcala(slug))
for kisa, uzunlar in SAGLAYICI.items():
if kisa in kume:
kume.update(uzunlar)
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
if not puanli:
return None
en = max(p[0] for p in puanli)
harfler = [s for s in ad if not s.isdigit()]
en_harf = max(p[4] for p in puanli)
if en_harf < len(harfler):
return None # adin bir sozcugu hicbir modelde yok: komut degil
iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3]))
if not iyi:
return None
if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme
return {"adaylar": [p[3] for p in iyi[:4]]}
if len(iyi) == 1:
return {"slug": iyi[0][2], "ad": iyi[0][3]}
tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model
if tam:
# Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan,
# sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur.
def oncelik(p):
return (p[2].startswith("ide/"), -p[5], "/" not in p[3])
tam.sort(key=oncelik)
if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]):
return {"slug": tam[0][2], "ad": tam[0][3]}
return {"adaylar": [p[3] for p in tam[:4]]}
return {"adaylar": [p[3] for p in iyi[:4]]}
_ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning",
"local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"}
def tur_ortasi(req, sira):
"""Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir:
once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir."""
girdi = req.get("input") or []
for oge in reversed(girdi[:max(sira, 0)]):
if not isinstance(oge, dict):
continue
tur = oge.get("type", "message")
if tur in _ARAC:
return True
if tur == "message":
if oge.get("role") == "assistant":
return oge.get("phase") == "commentary"
continue
return False
def metni_degistir(req, sira, yeni):
"""sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca <input> icini)."""
oge = req["input"][sira]
icerik = oge.get("content")
eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik)
if "<realtime_delegation>" in eski:
metin = re.sub(r"(<input>).*?(</input>)", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S)
else:
metin = yeni
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
def gecmisi_temizle(req, modeller, etkin=None):
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026).
etkin: sohbetin su an gercekten kullandigi model. Verilirse, en son kullanici mesajindaki komut (ardindan
arac ciktisi ya da sikistirma tetigi geldigi icin "gecmis" gorunen) yalniz bu modele gecirdiyse nota cevrilir;
uygulanmamis komut ("baglanti kapali" denen gibi) "degistirildi" diye anlatilmaz.
Doner: degistirilen mesaj sayisi."""
girdi = req.get("input")
if not isinstance(girdi, list):
return 0
son = son_sira(girdi)
son_kullanici = max((i for i, oge in enumerate(girdi) if isinstance(oge, dict)
and oge.get("type", "message") == "message" and oge.get("role") == "user"), default=-1)
n = 0
for i, oge in enumerate(girdi):
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
continue
metin, _, _ = son_kullanici_metni({"input": [oge]})
if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"):
continue
komut = komut_coz(metin)
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
if sonuc and "slug" in sonuc:
if etkin is not None and i == son_kullanici and sonuc["slug"] != etkin:
continue
metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; "
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
n += 1
return n
BILGI_ISARETI = "[Model değiştirme]"
BILGI = (BILGI_ISARETI + " Bu sohbetin modeli söylenerek değiştirilebilir: kullanıcı \"modeli <ad> yap\" dediğinde "
"(örnek: \"modeli gemini flash low yap\", \"modeli opus beş beş yap\") değişikliği aradaki köprü yapar; senin bir "
"araç çağırman ya da ayar değiştirmen gerekmez. Kullanıcı modeli değiştirip değiştiremeyeceğini sorarsa "
"\"değiştiremem\" deme; bu cümleyi söylemesinin yeterli olduğunu kısaca anlat. Hangi modelde olduğunu öğrenmek "
"için \"seçili model hangisi\" demesi yeter.")
NASIL = ("Hangi modele geçeyim? Adını söylemen yeterli, örneğin: \"modeli gemini flash low yap\" ya da "
"\"modeli opus beş beş yap\". Şu anki modeli öğrenmek için \"seçili model hangisi\" de.")
def bilgi_ekle(req):
"""Modele, sohbetin modelinin soylenerek degistirilebildigini bildirir (olcum 11 Ekim 2026: Opus "bu sohbetin
modelini buradan değiştiremem" dedi). Istegin basina tek developer mesaji; zaten varsa eklenmez. True: eklendi."""
girdi = req.get("input")
if not isinstance(girdi, list) or not girdi:
return False
for oge in girdi[:4]:
icerik = oge.get("content") if isinstance(oge, dict) else None
yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
if BILGI_ISARETI in yazi:
return False
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": BILGI}]})
return True
def devir_notu(yeni_ad, eski_ad):
return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren "
f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]")
class Yonlendirme:
"""sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir."""
def __init__(self, home):
self.yol = os.path.join(home, DOSYA) if home else None
self.veri = {}
if self.yol:
try:
with open(self.yol, encoding="utf-8") as f:
okunan = json.load(f)
if isinstance(okunan, dict):
self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")}
except (OSError, ValueError):
pass
def _yaz(self):
if not self.yol:
return
try:
gecici = self.yol + ".tmp"
with open(gecici, "w", encoding="utf-8") as f:
json.dump(self.veri, f, ensure_ascii=False)
os.replace(gecici, self.yol)
except OSError:
pass
def al(self, sohbet, gelen, simdi=None):
"""Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni
model secti) ya da kayit eskidiyse yonlendirme silinir."""
simdi = simdi or time.time()
with _KILIT:
k = self.veri.get(sohbet)
if not k:
return None
if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN:
del self.veri[sohbet]
self._yaz()
return None
if simdi - float(k.get("zaman") or 0) > YENILE_SN:
# Kullanilan yonlendirme eskimez: omur son kullanimdan sayilir (gunde en cok bir yazma).
k["zaman"] = simdi
self._yaz()
return k["hedef"]
def koy(self, sohbet, gelen, hedef, simdi=None):
simdi = simdi or time.time()
with _KILIT:
if hedef == gelen:
self.veri.pop(sohbet, None)
else:
self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi}
if len(self.veri) > EN_COK_KAYIT:
for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]:
del self.veri[eski]
self._yaz()
+20 -6
View File
@@ -266,10 +266,11 @@ def coz_istegi(oge, model, talimat="Sen bir konusma ozeti aktaricisisin."):
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.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 ----
@@ -281,22 +282,35 @@ DONGU_MESAJI = ("AstraBridge: Bu sohbetin baglami, ozetlendikten sonra da secili
class DonguFreni:
"""Ayni sohbet icin kisa surede cok sayida ozet istegini sayar."""
"""Ozet dongusunu durdurur, mesru sik sikistirmayi durdurmaz (10 Ekim 2026: kucuk esikli sohbette
10 dakikadaki 4. ozet reddedilip tur dusuyordu). Iki ayri sinir:
- art_arda: arada normal (ozet olmayan) istek gecmeden ust uste gelen ozet sayisi; gercek dongu budur.
- sinir: pencere_sn icindeki toplam ozet sayisi; her ozetin arasina normal istek giren yavas donguyu keser."""
def __init__(self, sinir=3, pencere_sn=600):
def __init__(self, sinir=8, pencere_sn=600, art_arda=3):
self.sinir = sinir
self.pencere = pencere_sn
self.art_arda = art_arda
self.kayit = {}
self.ust_uste = {}
def normal(self, anahtar):
"""Bu sohbette ozet olmayan bir istek gecti: art arda sayaci sifirlanir."""
self.ust_uste.pop(anahtar, None)
def izin(self, anahtar, simdi):
"""Bu ozet istegine izin var mi? Varsa kaydeder."""
zamanlar = [t for t in self.kayit.get(anahtar, []) if simdi - t < self.pencere]
if len(zamanlar) >= self.sinir:
if not zamanlar:
self.ust_uste.pop(anahtar, None) # pencere suresi doldu: eski art arda sayaci sohbeti kilitlemesin
if len(zamanlar) >= self.sinir or self.ust_uste.get(anahtar, 0) >= self.art_arda:
self.kayit[anahtar] = zamanlar
return False
zamanlar.append(simdi)
self.kayit[anahtar] = zamanlar
self.ust_uste[anahtar] = self.ust_uste.get(anahtar, 0) + 1
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.ust_uste.pop(k, None)
return True
+368
View File
@@ -0,0 +1,368 @@
"""Eklenen modellerin gercek baglam penceresi ve yetenekleri (10 Ekim 2026).
Neden: gecit model basina pencere vermiyor; kopru her modele abonelik kalibinin penceresini (272000)
kopyaliyordu. Codex, kucuk pencereli modele geciste sohbeti kendisi sikistirir, ama bunu ilan edilen
pencereye gore yapar (core/src/session/turn.rs maybe_run_previous_model_inline_compact). Pencere dogru
ilan edilince o yol kendiliginden calisir; yanlis ilan edilince istek hedefe sigmaz.
Kaynak sirasi (kucukten guvenilire degil, guvenilirden yedege):
1. Hedefin kendi hatasindan ogrenilen sinir (ogren / ogrenilen): en guvenilir, diske yazilir.
2. NanoGPT'nin acik model listesi (nano/... modelleri icin istegi tasiyan saglayicinin kendi degeri).
3. Saglayici kisaltmasina gore yerel tablo (cc, cx, ag).
4. Bilinmeyen: VARSAYILAN (temkinli) ve bir kez gunluk satiri.
Combo: yapraklarinin en kucugu (yedege dusulurse de sigsin).
Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asmamali.
"""
import json
import os
import re
import threading
import time
import urllib.request
TAVAN = 272_000 # abonelik modellerinin penceresi; ozetleyen model bundan fazlasini okuyamaz
VARSAYILAN = 128_000 # hakkinda hicbir sey bilinmeyen model
TABAN = 8_000 # saglayici verisi bozuksa (0, eksi) kullanilmaz
NANO_URL = "https://nano-gpt.com/api/v1/models?detailed=true"
NANO_OMUR_SN = 24 * 3600
NANO_DOSYA = "pencere-nano.json"
OGRENILEN_DOSYA = "pencere-ogrenilen.json"
ARACSIZ_DOSYA = "aracsiz-modeller.json"
# Saglayici kisaltmasi -> (pencere, gorsel girdi, arac cagrisi). cc = Claude Code aboneligi, cx = Codex
# aboneligi, ag = Antigravity. Degerler abonelik yollarinin temkinli sinirlaridir.
_YEREL = (
(re.compile(r"^cx/"), (272_000, True, True)),
(re.compile(r"^cc/"), (200_000, True, True)),
(re.compile(r"^ag/claude"), (200_000, True, True)),
(re.compile(r"^ag/gemini"), (1_000_000, True, True)),
(re.compile(r"^ag/gpt-oss"), (131_072, False, True)),
)
# "maximum context length is 131072 tokens", "prompt is too long: 3040251 tokens > 1000000 maximum",
# "context window of 32768 tokens", "exceeds the limit of 65536" ...
_SINIR = (
re.compile(r"maximum context length (?:is|of) (\d{4,8})", re.I),
re.compile(r"\d{4,9} tokens? > (\d{4,8}) maximum", re.I),
re.compile(r"context (?:window|length|size)(?: limit)? (?:is|of) (\d{4,8})", re.I),
re.compile(r"(?:limit|maximum) of (\d{4,8}) tokens", re.I),
re.compile(r"max(?:imum)? context tokens\D{0,6}(\d{4,8})", re.I),
)
_TASMA = re.compile(r"context[_ ](?:length|window)[_ ]exceeded|maximum context length|prompt is too long|"
r"context window|too many tokens|input is too long|exceeds? the (?:maximum |model.s )?context|max(?:imum)? context tokens", re.I)
def _oku(yol):
try:
with open(yol, encoding="utf-8") as f:
v = json.load(f)
return v if isinstance(v, dict) else {}
except (OSError, ValueError):
return {}
def _yaz(yol, veri):
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # es zamanli iki yazma ayni gecici dosyada carpismasin
with open(gecici, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol)
def nano_sadelestir(ham):
"""NanoGPT yaniti -> {model kimligi (kucuk harf): {"pencere", "gorsel", "arac"}}."""
cikti = {}
for m in (ham or {}).get("data") or []:
kimlik = str(m.get("id") or "").lower()
pencere = m.get("context_length")
if not kimlik or not isinstance(pencere, int) or pencere < TABAN:
continue
yetenek = m.get("capabilities") or {}
cikti[kimlik] = {"pencere": pencere, "gorsel": bool(yetenek.get("vision")),
"arac": bool(yetenek.get("tool_calling"))}
return cikti
def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir=None, ag=True):
"""Onbellek tazeyse onu, degilse agdan yenisini dondurur. Ag hatasinda eski onbellek (yasina bakilmadan)
kullanilir; o da yoksa bos sozluk doner ve cagiran yerel tabloya duser. Hicbir durumda istisna atmaz.
ag=False: aga cikilmaz, eldeki onbellek doner (koprunun acilisi beklemesin diye)."""
simdi = time.time() if simdi is None else simdi
yol = os.path.join(home, NANO_DOSYA) if home else ""
onbellek = _oku(yol) if yol else {}
try:
taze = simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN
except (TypeError, ValueError):
taze = False # bozuk "zaman" alani yenilemeyi engellemez
if onbellek.get("modeller") and taze:
return onbellek["modeller"]
if not ag:
return onbellek.get("modeller") or {}
try:
if getir is None:
istek = urllib.request.Request(NANO_URL, headers={"User-Agent": ua, "Accept": "application/json"})
with urllib.request.urlopen(istek, timeout=timeout) as resp:
ham = json.load(resp)
else:
ham = getir()
modeller = nano_sadelestir(ham)
if modeller:
if yol:
try:
_yaz(yol, {"zaman": simdi, "modeller": modeller})
except OSError:
pass
return modeller
except Exception: # noqa: BLE001 - ag, JSON, sertifika: hepsi "yerel tabloya dus" demek
pass
return onbellek.get("modeller") or {}
def ogrenilen(home):
return {k: int(v) for k, v in _oku(os.path.join(home, OGRENILEN_DOSYA)).items()
if isinstance(v, (int, float)) and v >= TABAN} if home else {}
def aracsizlar(home):
"""Hedefin "arac cagrisi desteklenmiyor" dedigi modeller (kimlik kumesi)."""
return {k for k, v in _oku(os.path.join(home, ARACSIZ_DOSYA)).items() if v} if home else set()
def aracsiz_mi(metin):
return bool(re.search(r"does not support tool|tool_choice_unsupported|tools? (?:are|is) not supported|"
r"function calling is not supported", metin or "", re.I))
def ogren_aracsiz(home, kimlik):
"""True: kayit yeni eklendi."""
if not home or not kimlik:
return False
yol = os.path.join(home, ARACSIZ_DOSYA)
veri = _oku(yol)
if veri.get(kimlik):
return False
veri[kimlik] = True
try:
_yaz(yol, veri)
except OSError:
return False
return True
def kapali_hata_mi(metin):
"""Gecit asil nedeni gizleyip yalniz "upstream 400" derse (olcum 11 Ekim 2026)."""
return bool(re.fullmatch(r"\s*upstream 4(?:00|13)\s*", metin or "", re.I))
_HIZ_SINIRI = re.compile(r"per minute|rate.?limit|tokens? per|\bTPM\b|\b429\b|too many requests", re.I)
def hiz_siniri_mi(metin):
"""Dakikalik hiz siniri hatasi baglam tasmasi degildir ("rate limit of 32768 tokens per minute")."""
return bool(_HIZ_SINIRI.search(metin or ""))
def tasma_mi(metin):
return bool(_TASMA.search(metin or "")) and not hiz_siniri_mi(metin)
def sinir_oku(metin):
"""Hedefin tasma hatasindaki gercek sinir; yoksa None."""
if hiz_siniri_mi(metin):
return None
for kalip in _SINIR:
m = kalip.search(metin or "")
if m and int(m.group(1)) >= TABAN:
return int(m.group(1))
return None
def ogren(home, kimlik, sinir):
"""Hedefin bildirdigi siniri kaydeder (yalniz kucultur). True: kayit degisti."""
if not home or not kimlik or not sinir or sinir < TABAN:
return False
yol = os.path.join(home, OGRENILEN_DOSYA)
veri = _oku(yol)
if isinstance(veri.get(kimlik), (int, float)) and veri[kimlik] <= sinir:
return False
veri[kimlik] = int(sinir)
try:
_yaz(yol, veri)
except OSError:
return False
return True
def yol_bilgisi(yol, nano):
""""nano/z-ai/glm-5.3" ya da "cc/claude-opus-5-5" -> {"pencere","gorsel","arac","kaynak"} ya da None."""
yol = (yol or "").strip()
if yol.startswith("9router/"):
yol = yol[len("9router/"):]
if yol.startswith("nano/"):
n = (nano or {}).get(yol[len("nano/"):].lower())
if n:
return {**n, "kaynak": "nanogpt"}
return None
for kalip, (pencere, gorsel, arac) in _YEREL:
if kalip.match(yol):
return {"pencere": pencere, "gorsel": gorsel, "arac": arac, "kaynak": "yerel"}
return None
def _yapraklar(dugum, derinlik=0):
if not isinstance(dugum, dict) or derinlik > 6:
return []
if dugum.get("kind") != "combo":
return [dugum.get("model")] if dugum.get("model") else []
cikti = []
for uye in dugum.get("members") or []:
cikti.extend(_yapraklar(uye, derinlik + 1))
return cikti
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
bilinmeyen = {"pencere": VARSAYILAN, "gorsel": False, "arac": True, "kaynak": "varsayilan"}
if tur == "combo":
yapraklar = _yapraklar((kayit or {}).get("topology"))
bilgiler = [yol_bilgisi(y, nano) or dict(bilinmeyen) for y in yapraklar]
if not bilgiler:
sonuc = dict(bilinmeyen)
else:
sonuc = {"pencere": min(b["pencere"] for b in bilgiler),
# Bir takim uyesi resmi goremese de digerleri gorur; sirali yedekte ilk uye belirler.
"gorsel": bool(bilgiler[0]["gorsel"]), "arac": bool(bilgiler[0]["arac"]),
"kaynak": "uyeler:" + ",".join(sorted({b["kaynak"] for b in bilgiler}))}
else:
if (yol or "").startswith("no-think/"):
# Dusunmesiz kopya: asil saglayici kimlikten okunur ("no-think-9router-cc-claude-..." -> cc).
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
if kimlik in aracsiz:
sonuc = {**sonuc, "arac": False}
sinir = (ogr or {}).get(kimlik)
if sinir and sinir < sonuc["pencere"]:
sonuc = {**sonuc, "pencere": int(sinir), "kaynak": "ogrenilen"}
sonuc["pencere"] = max(TABAN, min(TAVAN, int(sonuc["pencere"])))
return sonuc
def claude_var(tur, yol, kayit=None):
"""Istek Claude'a gidebilir mi? Tek modelde yolundan, comboda yapraklarindan okunur (Opus-5 gibi combolarin
adinda "claude" gecmez; token tahmini yine Claude oraniyla yapilmalidir)."""
yollar = _yapraklar((kayit or {}).get("topology")) if tur == "combo" else [yol]
for y in yollar:
y = str(y or "").lower()
if y.startswith("9router/"):
y = y[len("9router/"):]
if "claude" in y or y.startswith("cc/"):
return True
return False
def bekci_calisir(kaynak, pencere_token):
"""Tasma bekcisi bu pencereye guvenebilir mi? Hakkinda hicbir sey bilinmeyen modelde calismaz (tahmine
dayanarak gecmis kirpilmaz). Comboda tek bilinmeyen uye, bilinen KUCUK uyenin sinirini gecersiz kilmaz:
ilan edilen pencere VARSAYILAN'dan kucukse o deger bilinen bir uyeden gelmistir."""
kaynak = kaynak or "varsayilan"
if "varsayilan" not in kaynak:
return True
return kaynak.startswith("uyeler:") and kaynak != "uyeler:varsayilan" and int(pencere_token or 0) < VARSAYILAN
def kisa(pencere):
"""8192 -> "8K", 131072 -> "128K", 272000 -> "272K"."""
return f"{round(pencere / 1024) if pencere < 262_144 and pencere % 1024 == 0 else round(pencere / 1000)}K"
# ---- Istek oncesi tasma bekcisi ----
# Codex sikistirmadan sonra kullanici mesajlarini AYNEN tutar; cok buyuk bir yapistirma (ya da dev arac
# ciktisi) kucuk pencereli hedefe yine sigmaz ve hedef bos akisla doner (10 Ekim 2026 olcumu: 390 KB istek,
# 64K pencere, Codex 5 kez yeniden deneyip "stream closed" ile dustu). Bekci, hedefe giden KOPYADA en buyuk
# eski metin parcalarinin ortasini cikarir; yine sigmazsa istegi gondermeden anlasilir hata doner.
BAYT_BOLU_TOKEN = 3.2 # olcum (10 Ekim 2026): 540 KB duz Turkce metin = 171016 token (3,16 bayt/token)
DOLULUK = 0.97 # pencerenin bu oranini asan istek kirpilir / gonderilmez
KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
KIRP_BAS = KIRP_SON = 1500
# Claude ayni metni daha cok tokena boler. Olcum (11 Ekim 2026): 607 KB'lik gercek Codex istegi 3,2 oraniyla
# 189776 token tahmin edildi (pencere 200000) ve Claude tarafindan reddedildi; yani gercek oran 3,03'un altinda.
CLAUDE_BAYT_BOLU_TOKEN = 2.6
def oran_sec(hedef, claude=False):
"""claude=True: hedefin adinda gecmese de (combo) istek Claude'a gidebilir (bkz. claude_var)."""
return CLAUDE_BAYT_BOLU_TOKEN if (claude or "claude" in str(hedef or "").lower()) else BAYT_BOLU_TOKEN
def tahmini_token(req, oran=None):
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / (oran or BAYT_BOLU_TOKEN))
KOPRU_DEVAM_BASI = "(Önceki cevabın yarıda kaldı" # bridge.py claude_sonu_duzelt'in ekledigi mesajin basi
def _kopru_mesaji(oge):
icerik = oge.get("content")
if isinstance(icerik, list):
icerik = "".join(str(p.get("text") or "") for p in icerik if isinstance(p, dict))
return isinstance(icerik, str) and icerik.startswith(KOPRU_DEVAM_BASI)
def _son_arac_ciktisi(req):
"""En yeni arac ciktisinin metin sahipleri (kimlik kumesi): model bir sonraki adimi buna gore atar,
bu yuzden ancak baska hicbir sey yetmezse kirpilir."""
for it in reversed(req.get("input") or []):
if isinstance(it, dict) and "output" in it and str(it.get("type") or "").endswith("_output"):
deger = it.get("output")
return {id(it)} if isinstance(deger, str) else {id(p) for p in deger or [] if isinstance(p, dict)}
return set()
def _metin_parcalari(req):
"""Kirpilabilir (sahip, anahtar) ciftleri: arac ciktilari ile eski kullanici/asistan mesajlari.
Son kullanici mesajina ve talimat tasiyan developer/system mesajlarina (Codex kurallari, skill
yonergeleri, kanca baglami) dokunulmaz."""
girdi = req.get("input")
if not isinstance(girdi, list):
return []
# Koprunun sona ekledigi "devam et" mesaji kullanicinin son mesaji degildir; korunan, ondan onceki gercek mesajdir.
son_kullanici = max((i for i, it in enumerate(girdi) if isinstance(it, dict) and it.get("role") == "user"
and it.get("type", "message") == "message" and not _kopru_mesaji(it)), default=-1)
parcalar = []
for i, it in enumerate(girdi):
if not isinstance(it, dict) or i == son_kullanici or it.get("role") in ("developer", "system"):
continue
for alan in ("content", "output"):
deger = it.get(alan)
if isinstance(deger, str):
parcalar.append((it, alan))
elif isinstance(deger, list):
for p in deger:
if isinstance(p, dict) and isinstance(p.get("text"), str):
parcalar.append((p, "text"))
return parcalar
def sigdir(req, pencere_token, oran=None):
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
sinir = int(pencere_token * DOLULUK)
tahmin = tahmini_token(req, oran)
if not pencere_token or tahmin <= sinir:
return 0, True, tahmin
kirpilan = 0
en_yeni = _son_arac_ciktisi(req)
# Buyukten kucuge; en yeni arac ciktisi en sona kalir (yalniz digerleri yetmezse kirpilir).
parcalar = sorted(_metin_parcalari(req), key=lambda sa: (id(sa[0]) in en_yeni, -len(sa[0][sa[1]])))
for sahip, anahtar in parcalar:
metin = sahip[anahtar]
if len(metin) < KIRP_ESIK:
continue
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
kirpilan += 1
tahmin = tahmini_token(req, oran)
if tahmin <= sinir:
return kirpilan, True, tahmin
return kirpilan, tahmin <= sinir, tahmin
@@ -49,14 +49,18 @@ def _kisalt(metin):
+ metin[-yarim:])
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir."""
def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
"""(değiştirilen parça sayısı, kazanılan bayt). req['input'] yerinde değişir.
gorsel=True (hedef model resim görebiliyor): eski KULLANICI mesajlarındaki resimler de, en yeniden eskiye
toplam tut_bayt'ı aşmadıkça korunur (10 Ekim 2026: resim görebilen modele geçince "göremiyorum" diyordu).
Araç çıktılarındaki medya her durumda notla değiştirilir."""
inp = req.get('input')
if not isinstance(inp, list):
return 0, 0
son_kullanici = None
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
koru = set()
if son_kullanici is not None:
@@ -65,6 +69,20 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT):
medya = [p for p in icerik if _medya_mi(p)]
if medya and sum(_boyut(p) for p in medya) <= tut_bayt:
koru.add(son_kullanici)
if gorsel:
kalan = tut_bayt - sum(_boyut(p) for i in koru for p in inp[i].get('content') or [] if _medya_mi(p))
for i in range(len(inp) - 1, -1, -1):
it = inp[i]
if i in koru or not (isinstance(it, dict) and it.get('type', 'message') == 'message' and it.get('role') == 'user'):
continue
icerik = it.get('content')
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
if isinstance(icerik, list) else []
diger = [p for p in icerik if _medya_mi(p) and p not in resimler] if isinstance(icerik, list) else []
boyut = sum(_boyut(p) for p in resimler)
if resimler and not diger and boyut <= kalan:
koru.add(i)
kalan -= boyut
sayi = kazanc = 0
for i, it in enumerate(inp):
if not isinstance(it, dict) or i in koru:
@@ -31,7 +31,7 @@ class AccessPrograms(unittest.TestCase):
"fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(),
"log": lambda message: None,
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"DEBUG": False, "UA": "test", "RETRIES": 1, "UPSTREAM_IDLE_TIMEOUT": 3900,
}
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):
self.upstream = Mock()
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),
"urllib": types.SimpleNamespace(
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,
"SseRewriter": Mock(), "log": lambda message: None,
"DEBUG": False, "UA": "test", "RETRIES": 6, "UPSTREAM_IDLE_TIMEOUT": 3600,
"NO_RETRY_PATTERNS": [],
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "is_hatti": __import__("is_hatti"), "GORULEN_ISTEMCI": set(), "arac_semalarini_duzelt": lambda req: 0, "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
}
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), responses],
exec(compile(ast.Module(body=[pick("flatten_request"), pick("claude_sonu_duzelt"), pick("is_timeout"), pick("kopma_mi"), responses],
type_ignores=[]), "bridge.py", "exec"), self.env)
def send(self, model):
@@ -85,6 +85,16 @@ class ComboNotResent(unittest.TestCase):
self.assertEqual(self.upstream.call_count, 3)
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):
self.upstream.side_effect = [urllib.error.URLError(TimeoutError("timed out")), Mock(status=200)]
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()
@@ -0,0 +1,114 @@
"""Bridge 7: tool-less models are learned, flat gateway errors are translated, instructions are never trimmed."""
import ast
import json
import tempfile
import unittest
from pathlib import Path
import pencere
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "nokta_mesaji_mi", "hazir_yaniti",
"sema_duzelt", "arac_semalarini_duzelt"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI", "NOKTA_ISARETLERI"} for t in node.targets):
nodes.insert(0, node)
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
def feed(event, **kw):
adapter = env["SseRewriter"](set(), **kw)
raw = b"data: " + json.dumps(event).encode() + b"\n\n"
return [json.loads(line[6:]) for line in (adapter.feed(raw) + adapter.flush()).decode().splitlines()
if line.startswith("data: ")]
class Ek(unittest.TestCase):
def test_model_without_tool_calls_is_reported_and_remembered(self):
seen = []
flat = {"type": "error", "code": "bad_request", "status_code": 400, "message":
'[400]: {"error":{"message":"This model does not support tool calls.","code":"tool_choice_unsupported"}}'}
out = feed(flat, hedef="m", aracsiz=seen.append)
# A permanent error ends the turn with a visible answer instead of a silent retry.
self.assertEqual([e["type"] for e in out], ["response.output_item.added", "response.output_text.delta",
"response.output_item.done", "response.completed"])
self.assertIn("araç çağrısını desteklemiyor", out[1]["delta"])
self.assertEqual(out[2]["item"]["phase"], "final_answer")
self.assertEqual(seen, ["m"])
home = tempfile.mkdtemp()
self.assertTrue(pencere.ogren_aracsiz(home, "m"))
self.assertFalse(pencere.ogren_aracsiz(home, "m"))
self.assertFalse(pencere.coz("m", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
self.assertTrue(pencere.coz("n", "model", "cc/claude-opus-5-5", aracsiz=pencere.aracsizlar(home))["arac"])
def test_flat_overflow_error_is_translated_and_learned(self):
seen = []
flat = {"type": "error", "code": "bad_request", "message":
"Your prompt exceeds the model's context length. Max context tokens: 65536."}
out = feed(flat, hedef="m", tasma=lambda hedef, metin: seen.append(pencere.sinir_oku(metin)))
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
self.assertEqual(seen, [65536])
self.assertEqual(feed({"type": "error", "code": "server_error", "message": "upstream 503"})[0]["type"], "error")
def test_guard_never_trims_instruction_messages(self):
big = "KURAL " * 40000
req = {"input": [
{"type": "message", "role": "developer", "content": [{"type": "input_text", "text": big}]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "eski " * 40000}]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "soru"}]}]}
pencere.sigdir(req, 65536)
self.assertEqual(req["input"][0]["content"][0]["text"], big)
self.assertIn("çıkarıldı (köprü)", req["input"][1]["content"][0]["text"])
class Sema(unittest.TestCase):
def test_top_level_union_becomes_one_object_schema(self):
req = {"tools": [
{"type": "function", "name": "a", "parameters": {"oneOf": [
{"type": "object", "properties": {"x": {"type": "string"}, "k": {"type": "string"}}, "required": ["x", "k"]},
{"type": "object", "properties": {"y": {"type": "integer"}, "k": {"type": "string"}}, "required": ["y", "k"]}]}},
{"type": "function", "name": "b", "parameters": {"type": "object", "properties": {"z": {"anyOf": [{"type": "string"}, {"type": "null"}]}}}},
{"type": "function", "name": "c", "parameters": {"allOf": [
{"type": "object", "properties": {"p": {"type": "string"}}, "required": ["p"]},
{"type": "object", "properties": {"q": {"type": "string"}}, "required": ["q"]}]}}]}
self.assertEqual(env["arac_semalarini_duzelt"](req), 2)
a, b, c = (t["parameters"] for t in req["tools"])
self.assertEqual((a["type"], sorted(a["properties"]), a["required"]), ("object", ["k", "x", "y"], ["k"]))
self.assertNotIn("oneOf", a)
self.assertIn("anyOf", b["properties"]["z"]) # nested unions are left alone
self.assertEqual((sorted(c["properties"]), c["required"]), (["p", "q"], ["p", "q"]))
self.assertEqual(env["arac_semalarini_duzelt"](req), 0)
class Nokta(unittest.TestCase):
def user(self, content):
return {"type": "message", "role": "user", "content": content}
def test_only_a_lone_dot_from_the_user_is_answered_locally(self):
is_dot = env["nokta_mesaji_mi"]
self.assertTrue(is_dot({"input": [self.user([{"type": "input_text", "text": " . "}])]}))
self.assertTrue(is_dot({"input": [self.user("x"), self.user(".")]}))
for other in ("..", "?", "Selam", "", "1."):
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": other}])]}), other)
self.assertFalse(is_dot({"input": [self.user("."), {"type": "function_call_output", "call_id": "c", "output": "x"}]}))
self.assertFalse(is_dot({"input": [self.user([{"type": "input_text", "text": "."}, {"type": "input_image", "image_url": "x"}])]}))
self.assertFalse(is_dot({"input": [{"type": "message", "role": "assistant", "content": "."}]}))
self.assertFalse(is_dot({"input": []}))
def test_local_answer_is_a_complete_response_with_a_final_answer(self):
raw = env["hazir_yaniti"]("ide/x", "cc/opus-5-5 hazır.").decode()
events = [json.loads(line[6:]) for line in raw.splitlines() if line.startswith("data: ")]
self.assertEqual([e["type"] for e in events], ["response.created", "response.output_item.added",
"response.output_text.delta", "response.output_item.done", "response.completed"])
done = events[-1]["response"]
self.assertEqual(done["status"], "completed")
self.assertEqual(done["output"][0]["phase"], "final_answer")
self.assertEqual(done["output"][0]["content"][0]["text"], "cc/opus-5-5 hazır.")
self.assertEqual(done["usage"]["total_tokens"], 0)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,154 @@
"""Bridge 8: "modeli X yap" is handled by the bridge; a per-chat route survives until the user picks another model."""
import tempfile
import unittest
import model_gecis as g
MODELS = [("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("gpt-6.1-sol", "GPT-6.1-Sol"),
("ide/9router-ag-gemini-3.8-flash-low", "agy/gemini-3.8-flash-low"),
("ide/9router-ag-gemini-3.8-flash-high", "agy/gemini-3.8-flash-high"),
("ide/9router-cc-claude-opus-5-5", "cc/opus-5-5"), ("ide/9router-cc-claude-opus-5-5-nt", "cc/opus-5-5 (no-think)"),
("ide/9router-nano-z-ai-glm-5.3-flash-uncensored", "nano/glm-5.3-flash-uncensored"),
("ide/agnes-agnes-2.0-flash", "agnes/agnes-2.0-flash"), ("ide/agnes-agnes-2.5-flash", "agnes/agnes-2.5-flash")]
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
def voice(text):
return user(f"<realtime_delegation>\n <input>{text}</input>\n <transcript_delta>user: x</transcript_delta>\n</realtime_delegation>")
def pick(text):
command = g.komut_coz(text)
return g.eslestir(command["ad"], MODELS) if command and command["tur"] == "gec" else command
class Komut(unittest.TestCase):
def test_spoken_names_resolve_to_one_model(self):
for text, slug in [("modeli gemini flash low yap", "ide/9router-ag-gemini-3.8-flash-low"),
("Modeli Gemini 3.8 Flash High'a geçir", "ide/9router-ag-gemini-3.8-flash-high"),
("modeli klod opus 5 5 yap", "ide/9router-cc-claude-opus-5-5"),
("modeli opus 5 5'e geç", "ide/9router-cc-claude-opus-5-5"),
("modeli gpt 6 astra yap", "gpt-6-astra"), ("model geç gpt 6.1 sol", "gpt-6.1-sol"),
("modeli glm flaş sansürsüz yap", "ide/9router-nano-z-ai-glm-5.3-flash-uncensored")]:
self.assertEqual((pick(text) or {}).get("slug"), slug, text)
def test_ambiguous_name_lists_candidates_and_changes_nothing(self):
self.assertEqual(pick("modeli agnes yap"), {"adaylar": ["agnes/agnes-2.0-flash", "agnes/agnes-2.5-flash"]})
self.assertIn("adaylar", pick("modeli klod opus yap")) # two Opus entries: ask
self.assertIn("adaylar", pick("modeli sol yap")) # GPT-6-Sol or GPT-6.1-Sol: ask
self.assertIn("adaylar", pick("modeli gpt 999 astra yap")) # words fit, version does not: ask
def test_negations_and_general_questions_are_not_commands(self):
for text in ("modeli gemini flash low yapma", "modeli değiştirme opus kalsın", "GPT 6 Astra modeli için test yap",
"hangi model daha ucuz", "en iyi model hangisi", "modeli seçili dosyaya göre gemini flash low yap"):
result = pick(text)
self.assertFalse(result and ("slug" in result or result.get("tur") == "sor"), text)
self.assertEqual(g.komut_coz("hangi modelsin"), {"tur": "sor"})
def test_ordinary_sentences_are_not_commands(self):
for text in ("Masaüstünde kaç klasör var", "bu modeli kullanarak bir site yap", "yapay zeka modeli nedir",
"hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
"model " + "çok uzun bir cümle " * 5 + "yap"):
self.assertFalse(pick(text) and "slug" in pick(text), text)
self.assertEqual(g.komut_coz("seçili model hangisi"), {"tur": "sor"})
self.assertEqual(g.komut_coz("bu chatte ana model kim"), {"tur": "sor"})
def test_voice_delegation_yields_only_the_spoken_input(self):
text, index, spoken = g.son_kullanici_metni({"input": [user("a"), voice("modeli opus yap")]})
self.assertEqual((text, index, spoken), ("modeli opus yap", 1, True))
self.assertEqual(g.son_kullanici_metni({"input": [user("<skill>x</skill>")]})[0], None)
self.assertEqual(g.son_kullanici_metni({"input": [{"type": "function_call_output", "output": "x"}]})[0], None)
class TurOrtasi(unittest.TestCase):
def test_message_after_tool_items_is_mid_turn_and_after_final_answer_is_not(self):
final = {"type": "message", "role": "assistant", "phase": "final_answer", "content": "ok"}
call = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
out = {"type": "function_call_output", "call_id": "c", "output": "x"}
self.assertFalse(g.tur_ortasi({"input": [user("a"), final, user(".")]}, 2))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, user(".")]}, 3))
self.assertTrue(g.tur_ortasi({"input": [user("a"), call, out, voice("bekliyorum"), user(".")]}, 4))
self.assertTrue(g.tur_ortasi({"input": [user("a"), {"type": "message", "role": "assistant", "phase": "commentary",
"content": "bakıyorum"}, user(".")]}, 2))
self.assertFalse(g.tur_ortasi({"input": [user(".")]}, 0))
def test_handover_note_replaces_only_the_spoken_input(self):
req = {"input": [voice("modeli opus yap")]}
g.metni_degistir(req, 0, g.devir_notu("cc/opus-5-5", "GPT-6-Sol"))
text = req["input"][0]["content"][0]["text"]
self.assertIn("<transcript_delta>", text)
self.assertIn("cc/opus-5-5 olarak devam ediyorum", text)
self.assertNotIn("modeli opus yap</input>", text)
class Gecmis(unittest.TestCase):
def test_handled_commands_in_history_become_notes_but_the_last_message_is_left_alone(self):
req = {"input": [user("modeli gemini flash low yap"), user("Masaüstünde kaç dosya var"),
voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
self.assertIn("agy/gemini-3.8-flash-low olarak değiştirdi", req["input"][0]["content"][0]["text"])
self.assertEqual(req["input"][1]["content"][0]["text"], "Masaüstünde kaç dosya var")
self.assertIn("<transcript_delta>", req["input"][2]["content"][0]["text"])
self.assertEqual(req["input"][3]["content"][0]["text"], "modeli gpt 6 astra yap")
self.assertEqual(g.gecmisi_temizle(req, MODELS), 0)
class Bilgi(unittest.TestCase):
def test_model_is_told_once_that_the_chat_model_changes_by_saying_so(self):
req = {"input": [user("selam")]}
self.assertTrue(g.bilgi_ekle(req))
self.assertEqual(req["input"][0]["role"], "developer")
self.assertIn("modeli <ad> yap", req["input"][0]["content"][0]["text"])
self.assertFalse(g.bilgi_ekle(req))
self.assertEqual(len(req["input"]), 2)
self.assertFalse(g.bilgi_ekle({"input": []}))
def test_a_command_without_a_name_asks_which_model(self):
for text in ("modeli değiştir", "başka modele geç", "modeli değiştirir misin"):
self.assertEqual(g.komut_coz(text), {"tur": "nasil"}, text)
self.assertIsNone(g.komut_coz("modeli değiştirme"))
class Dogal(unittest.TestCase):
GECIT = [("ide/9router/ModelARK/dola-seed-2-1-turbo-260628", "9router/ModelARK/dola-seed-2-1-turbo-260628"),
("ide/9router/ModelARK/seed-2-0-pro-260328", "9router/ModelARK/seed-2-0-pro-260328")]
def test_command_without_the_word_model_and_a_spelled_out_name(self):
for text in ("Seed iki nokta bir turbo yap", "Evet. Seed, S E E D iki nokta bir turbo yap"):
command = g.komut_coz(text)
self.assertEqual((command["ad"], command["kesin"]), (["seed", "2", "1", "turbo"], False), text)
self.assertEqual(g.eslestir(command["ad"], self.GECIT)["slug"], "ide/9router/ModelARK/dola-seed-2-1-turbo-260628")
self.assertTrue(g.komut_coz("Modeli Seed iki nokta bir turbo yap")["kesin"])
self.assertEqual(g.eslestir(g.komut_coz("gemini flash low'a geç")["ad"], MODELS)["slug"],
"ide/9router-ag-gemini-3.8-flash-low")
def test_ordinary_requests_ending_in_a_verb_are_not_commands(self):
for text in ("test yap", "kahve yap", "yapma"):
self.assertIsNone(g.komut_coz(text), text)
for text in ("şu dosyayı düzelt ve commit yap", "rapor hazırla gemini flash low ile karşılaştırma yap"):
command = g.komut_coz(text)
self.assertFalse(command["kesin"])
self.assertFalse((g.eslestir(command["ad"], MODELS) or {}).get("slug"), text)
class Yon(unittest.TestCase):
def test_route_lasts_until_the_user_picks_another_model_in_the_list(self):
with tempfile.TemporaryDirectory() as home:
y = g.Yonlendirme(home)
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
self.assertEqual(g.Yonlendirme(home).al("chat", "gpt-6-sol", simdi=1001), "ide/x") # survives a restart
self.assertIsNone(y.al("other", "gpt-6-sol", simdi=1001))
self.assertIsNone(y.al("chat", "gpt-6-astra", simdi=1002)) # user picked another
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1003)) # and it stays cleared
y.koy("chat", "gpt-6-sol", "ide/x", simdi=1000)
y.koy("chat", "gpt-6-sol", "gpt-6-sol", simdi=1001) # back to the picked one
self.assertIsNone(y.al("chat", "gpt-6-sol", simdi=1002))
y.koy("old", "a", "b", simdi=1000)
self.assertIsNone(y.al("old", "a", simdi=1000 + g.OMUR_SN + 1))
if __name__ == "__main__":
unittest.main()
@@ -7,12 +7,13 @@ import unittest
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter"}
names = {"flatten_request", "fix_tool_ids", "drop_foreign_items", "SseRewriter", "phase_damgala",
"asistan_mesaji_mi"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
env = {"json": json, "log": lambda message: None}
env = {"json": json, "re": __import__("re"), "log": lambda message: None, "pencere": __import__("pencere")}
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES"} for t in node.targets):
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
nodes.insert(0, node)
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
@@ -162,11 +162,22 @@ class Akis(unittest.TestCase):
class Fren(unittest.TestCase):
def test_ayni_sohbette_ust_uste_ozet_durdurulur(self):
f = o.DonguFreni(sinir=3, pencere_sn=600)
def test_tight_loop_is_stopped_but_spaced_summaries_are_not(self):
f = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
# Summary after summary with no normal request in between: a real loop.
self.assertEqual([f.izin("sohbet-a", t) for t in (0, 10, 20, 30, 40)], [True, True, True, False, False])
self.assertTrue(f.izin("sohbet-b", 40)) # baska sohbet etkilenmez
self.assertTrue(f.izin("sohbet-a", 700)) # pencere gecince yeniden izin
self.assertTrue(f.izin("sohbet-b", 40)) # another conversation is unaffected
# A normal request between summaries (small-window model compacting every turn) keeps working.
g = o.DonguFreni(sinir=8, pencere_sn=600, art_arda=3)
izinler = []
for t in range(0, 70, 10):
izinler.append(g.izin("sohbet-c", t))
g.normal("sohbet-c")
self.assertEqual(izinler, [True] * 7)
self.assertTrue(g.izin("sohbet-c", 70)) # the 8th in the window
g.normal("sohbet-c")
self.assertFalse(g.izin("sohbet-c", 80)) # the total cap still ends a slow loop
self.assertTrue(g.izin("sohbet-c", 700)) # and lifts once the window passes
self.assertIn("Dongu durduruldu", o.DONGU_MESAJI % 3)
@@ -0,0 +1,253 @@
"""Bridge version 7: real context windows, phase stamping, overflow errors, images for vision models."""
import ast
import json
import os
import tempfile
import unittest
from pathlib import Path
import ide_catalog as c
import pencere
import resim_context
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
names = {"fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi"}
nodes = [n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.ClassDef)) and n.name in names]
for node in tree.body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id in
{"ID_PREFIX", "FOREIGN_ID_MARK", "REFERENCE_TYPES", "INLINE_TYPES", "ARAC_OGELERI"} for t in node.targets):
nodes.insert(0, node)
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
NANO = pencere.nano_sadelestir({"data": [
{"id": "z-ai/glm-5.3-flash-uncensored", "context_length": 1048576,
"capabilities": {"vision": True, "tool_calling": True}},
{"id": "mlabonne/NeuralDaredevil-8B-abliterated", "context_length": 8192,
"capabilities": {"vision": False, "tool_calling": False}},
{"id": "nousresearch/hermes-3-llama-3.1-70b", "context_length": 65536,
"capabilities": {"vision": False, "tool_calling": True}},
{"id": "broken", "context_length": 0}]})
def sse(*events):
return b"".join(b"event: " + e["type"].encode() + b"\ndata: " + json.dumps(e).encode() + b"\n\n" for e in events)
def parse(raw):
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
def message(text, ident="msg_1"):
return {"type": "message", "id": ident, "role": "assistant", "content": [{"type": "output_text", "text": text}]}
class Windows(unittest.TestCase):
def test_provider_data_local_table_and_cap(self):
coz = pencere.coz
# The provider that carries the request decides; nothing is declared above the summarizer's window.
self.assertEqual(coz("9router-nano-z-ai-glm-5.3-flash-uncensored", "model", "nano/z-ai/glm-5.3-flash-uncensored",
nano=NANO)["pencere"], pencere.TAVAN)
small = coz("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "model",
"nano/mlabonne/NeuralDaredevil-8B-abliterated", nano=NANO)
self.assertEqual((small["pencere"], small["arac"], small["gorsel"]), (8192, False, False))
self.assertEqual(coz("9router-cc-claude-opus-5-5", "model", "cc/claude-opus-5-5")["pencere"], 200000)
self.assertEqual(coz("9router-cx-gpt-6-astra", "model", "cx/gpt-6-astra")["pencere"], 272000)
self.assertEqual(coz("no-think-9router-cc-claude-opus-5-5", "model", "no-think/claude-opus-5-5")["pencere"], 200000)
unknown = coz("agnes-agnes-2.5-flash", "model", "Agnes 2.5 Flash", nano=NANO)
self.assertEqual((unknown["pencere"], unknown["kaynak"]), (pencere.VARSAYILAN, "varsayilan"))
self.assertNotIn("broken", NANO)
def test_combo_declares_its_smallest_member(self):
combo = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "cx/gpt-6-astra"},
{"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
{"kind": "model", "model": "nano/nousresearch/hermes-3-llama-3.1-70b"}]}]}}
self.assertEqual(pencere.coz("X", "combo", "X", combo, NANO)["pencere"], 65536)
self.assertEqual(pencere.coz("X", "combo", "X", None, NANO)["pencere"], pencere.VARSAYILAN)
def test_limit_learned_from_the_target_error_only_shrinks(self):
home = tempfile.mkdtemp()
text = "This model's maximum context length is 32768 tokens. However, you requested 51234 tokens"
self.assertTrue(pencere.tasma_mi(text))
self.assertEqual(pencere.sinir_oku(text), 32768)
self.assertEqual(pencere.sinir_oku("prompt is too long: 3040251 tokens > 1000000 maximum"), 1000000)
gateway = ("Your prompt exceeds the model's context length. Estimated input tokens: 171016. "
"Max context tokens: 65536.")
self.assertTrue(pencere.tasma_mi(gateway))
self.assertEqual(pencere.sinir_oku(gateway), 65536)
self.assertIsNone(pencere.sinir_oku("rate limit exceeded"))
self.assertFalse(pencere.tasma_mi("rate limit exceeded"))
self.assertTrue(pencere.ogren(home, "m", 32768))
self.assertFalse(pencere.ogren(home, "m", 65536)) # a larger number never overrides
self.assertEqual(pencere.ogrenilen(home), {"m": 32768})
self.assertEqual(pencere.coz("m", "model", "cc/claude-opus-5-5", ogr=pencere.ogrenilen(home))["pencere"], 32768)
def test_nano_cache_survives_a_network_failure(self):
home = tempfile.mkdtemp()
fresh = {"data": [{"id": "A/b", "context_length": 16384, "capabilities": {}}]}
self.assertEqual(pencere.nano_yukle(home, simdi=1000, getir=lambda: fresh)["a/b"]["pencere"], 16384)
def fail():
raise OSError("offline")
later = 1000 + pencere.NANO_OMUR_SN + 5
self.assertEqual(pencere.nano_yukle(home, simdi=later, getir=fail)["a/b"]["pencere"], 16384)
self.assertEqual(pencere.nano_yukle(tempfile.mkdtemp(), simdi=later, getir=fail), {})
self.assertEqual((pencere.kisa(8192), pencere.kisa(131072), pencere.kisa(200000)), ("8K", "128K", "200K"))
def test_description_line_warns_about_small_window_and_no_tools(self):
bilgi = {"pencere": 8192, "arac": False, "gorsel": False}
line = c.aciklama("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "9R · nano/NeuralDaredevil-8B-abliterated",
"model", None, bilgi)
self.assertEqual(line, "NanoGPT · 8K · araçsız") # warnings first; the rest no longer fits in 30
self.assertLessEqual(len(line), c.ACIKLAMA_SINIRI)
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model", None,
{"pencere": 200000, "arac": True, "gorsel": True}), "Tek model · Claude")
class Guard(unittest.TestCase):
def req(self, old, last="son soru"):
return {"instructions": "talimat", "input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": old}]},
{"type": "function_call_output", "call_id": "c", "output": "cikti " * 4000},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": last}]}]}
def test_request_that_fits_is_untouched(self):
req = self.req("kisa")
before = json.dumps(req)
self.assertEqual(pencere.sigdir(req, 272000)[:2], (0, True))
self.assertEqual(json.dumps(req), before)
def test_oversized_old_parts_lose_their_middle_and_the_last_user_message_never_does(self):
req = self.req("BAS " + "dolgu " * 60000 + " SON", last="yeni " * 3000)
kirpilan, sigdi, tahmin = pencere.sigdir(req, 65536)
self.assertTrue(sigdi)
self.assertGreaterEqual(kirpilan, 1)
old = req["input"][0]["content"][0]["text"]
self.assertTrue(old.startswith("BAS ") and old.endswith(" SON"))
self.assertIn("çıkarıldı (köprü)", old)
self.assertEqual(req["input"][2]["content"][0]["text"], "yeni " * 3000) # the new message is intact
self.assertLessEqual(tahmin, int(65536 * pencere.DOLULUK))
def test_a_last_message_larger_than_the_window_is_reported_not_sent(self):
req = self.req("kisa", last="x" * 400000)
self.assertFalse(pencere.sigdir(req, 32768)[1])
class Phase(unittest.TestCase):
def run_stream(self, *events, chunk=37):
adapter = env["SseRewriter"](set())
raw = sse(*events)
out = b"".join(adapter.feed(raw[i:i + chunk]) for i in range(0, len(raw), chunk)) + adapter.flush()
return parse(out)
def test_message_before_a_tool_call_is_commentary(self):
call = {"type": "function_call", "id": "fc_1", "call_id": "call_1", "name": "shell", "arguments": "{}"}
out = self.run_stream(
{"type": "response.output_item.added", "item": message("")},
{"type": "response.output_text.delta", "item_id": "msg_1", "delta": "bakiyorum"},
{"type": "response.output_item.done", "item": message("bakiyorum")},
{"type": "response.output_item.added", "item": call},
{"type": "response.output_item.done", "item": call},
{"type": "response.completed", "response": {"output": [message("bakiyorum"), call]}})
self.assertEqual([e["type"] for e in out], [
"response.output_item.added", "response.output_text.delta", "response.output_item.done",
"response.output_item.added", "response.output_item.done", "response.completed"]) # order kept
self.assertEqual(out[2]["item"]["phase"], "commentary")
self.assertEqual(out[5]["response"]["output"][0]["phase"], "commentary")
self.assertNotIn("phase", out[5]["response"]["output"][1])
def test_last_message_without_tool_calls_is_the_final_answer(self):
out = self.run_stream(
{"type": "response.output_item.done", "item": message("ara", "msg_a")},
{"type": "response.output_item.added", "item": message("", "msg_b")},
{"type": "response.output_item.done", "item": message("son", "msg_b")},
{"type": "response.completed", "response": {"output": [message("ara", "msg_a"), message("son", "msg_b")]}})
done = [e for e in out if e["type"] == "response.output_item.done"]
self.assertEqual([e["item"]["phase"] for e in done], ["commentary", "final_answer"])
self.assertEqual([o["phase"] for o in out[-1]["response"]["output"]], ["commentary", "final_answer"])
self.assertEqual(out[-1]["type"], "response.completed") # done comes first
def test_existing_phase_and_cut_streams_are_left_alone(self):
native = dict(message("x"), phase="final_answer")
out = self.run_stream({"type": "response.output_item.done", "item": native},
{"type": "response.completed", "response": {"output": [dict(native)]}})
self.assertEqual(out[0]["item"]["phase"], "final_answer")
# No terminal event: the held message is still delivered, unmarked rather than guessed.
cut = self.run_stream({"type": "response.output_item.done", "item": message("yarim")})
self.assertEqual(len(cut), 1)
self.assertNotIn("phase", cut[0]["item"])
def test_overflow_failure_gets_the_code_codex_understands(self):
seen = []
adapter = env["SseRewriter"](set(), hedef="model-x", tasma=lambda hedef, metin: seen.append((hedef, metin)))
failed = {"type": "response.failed", "response": {"error": {
"code": "server_error", "message": "maximum context length is 32768 tokens"}}}
out = parse(adapter.feed(sse(failed)) + adapter.flush())
self.assertEqual(out[0]["response"]["error"]["code"], "context_length_exceeded")
self.assertEqual(seen[0][0], "model-x")
other = {"type": "response.failed", "response": {"error": {"code": "rate_limit", "message": "slow down"}}}
self.assertEqual(parse(adapter.feed(sse(other)))[0]["response"]["error"]["code"], "rate_limit")
class GatewayErrors(unittest.TestCase):
def test_permanent_gateway_error_becomes_a_failure_codex_shows_at_once(self):
adapter = env["SseRewriter"](set())
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 402"}})
out = parse(adapter.feed(raw) + adapter.flush())
self.assertEqual(out[-1]["type"], "response.completed") # visible answer, turn ends normally
self.assertIn("kredi ya da kota", out[1]["delta"])
self.assertIn("upstream 402", out[1]["delta"])
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 400"}})
out = parse(env["SseRewriter"](set()).feed(raw))
self.assertIn("nedenini bildirmedi", out[1]["delta"])
self.assertEqual(out[-1]["type"], "response.completed")
# A transient error is left alone so that Codex retries it.
raw = sse({"type": "error", "error": {"type": "upstream_error", "message": "upstream 503"}})
self.assertEqual(parse(env["SseRewriter"](set()).feed(raw))[0]["type"], "error")
class Images(unittest.TestCase):
def req(self):
img = {"type": "input_image", "image_url": "data:image/png;base64," + "A" * 2000}
return {"input": [
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "bak"}, dict(img)]},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "OKUDUM"}]},
{"type": "function_call_output", "call_id": "c", "output": [dict(img)]},
{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "ne yaziyordu"}]}]}
def test_text_model_gets_notes_vision_model_keeps_user_images(self):
text_only = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(text_only)[0], 2)
self.assertNotIn("input_image", json.dumps(text_only))
vision = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(vision, gorsel=True)[0], 1) # only the tool output
self.assertEqual(vision["input"][0]["content"][1]["type"], "input_image")
self.assertNotIn("input_image", json.dumps(vision["input"][2]))
big = self.req()
self.assertEqual(resim_context.resimleri_sadelestir(big, tut_bayt=500, gorsel=True)[0], 2) # over budget
class ClaudeOrani(unittest.TestCase):
def test_claude_targets_are_estimated_more_strictly_than_others(self):
import pencere as p
req = {"input": [{"type": "function_call_output", "call_id": "c", "output": "x" * 600_000},
{"type": "message", "role": "user", "content": "son"}]}
self.assertEqual(p.oran_sec("9router-cc-claude-opus-5-5"), p.CLAUDE_BAYT_BOLU_TOKEN)
self.assertEqual(p.oran_sec("9router-ag-gemini-3.8-flash-low"), p.BAYT_BOLU_TOKEN)
self.assertEqual(p.sigdir({"input": [dict(i) for i in req["input"]]}, 200_000)[0], 0) # 187500: fits
trimmed, fits, estimate = p.sigdir(req, 200_000, p.oran_sec("cc-claude-opus")) # 230769: trimmed
self.assertEqual((trimmed, fits), (1, True))
self.assertLess(estimate, 194_000)
self.assertTrue(p.kapali_hata_mi("upstream 400"))
# A per-minute rate limit is neither an overflow nor a window size to learn.
rate = "Rate limit reached: limit of 32768 tokens per minute, context window ok"
self.assertFalse(p.tasma_mi(rate))
self.assertIsNone(p.sinir_oku(rate))
self.assertTrue(p.tasma_mi("maximum context length is 65536 tokens"))
self.assertFalse(p.kapali_hata_mi("upstream 400: invalid tool schema"))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,74 @@
"""An answer cut by the client is carried to the next turn of the same conversation, once."""
import json
import unittest
import yarim
def delta(text):
return (b"event: response.output_text.delta\ndata: "
+ json.dumps({"type": "response.output_text.delta", "delta": text}).encode() + b"\n\n")
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
class Yarim(unittest.TestCase):
def setUp(self):
yarim._KAYIT.clear()
def cut(self, key="sohbet-1", text="1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"):
izle = yarim.Izleyici(key, "ide/x")
raw = delta(text[:20]) + delta(text[20:])
for i in range(0, len(raw), 17):
izle.feed(raw[i:i + 17])
return izle
def test_cut_answer_reaches_the_next_user_message_once(self):
izle = self.cut()
self.assertTrue(yarim.kaydet(izle, simdi=100))
req = {"input": [user("liste yaz"), user("kaldigin yerden devam et")]}
self.assertGreater(yarim.uygula(req, "sohbet-1", simdi=200), 0)
note = req["input"][-1]["content"][-1]["text"]
self.assertIn("3. uc", note)
self.assertIn('model="ide/x"', note)
self.assertEqual(req["input"][-1]["content"][0]["text"], "kaldigin yerden devam et") # the user's words stay first
again = {"input": [user("bir daha")]}
self.assertEqual(yarim.uygula(again, "sohbet-1", simdi=300), 0) # used once
self.assertEqual(yarim.uygula({"input": [user("x")]}, "baska-sohbet", simdi=300), 0)
def test_finished_short_or_keyless_answers_are_not_kept(self):
done = self.cut()
done.feed(b'event: response.completed\ndata: {"type": "response.completed"}\n\n')
self.assertFalse(yarim.kaydet(done))
self.assertFalse(yarim.kaydet(self.cut(text="kisa")))
self.assertFalse(yarim.kaydet(self.cut(key="")))
def test_note_waits_for_a_real_user_turn_and_skips_text_codex_already_saved(self):
yarim.kaydet(self.cut(), simdi=100)
tool_turn = {"input": [user("x"), {"type": "function_call_output", "call_id": "c", "output": "ok"}]}
self.assertEqual(yarim.uygula(tool_turn, "sohbet-1", simdi=110), 0)
compaction = {"input": [user("x"), {"type": "compaction_trigger"}]}
self.assertEqual(yarim.uygula(compaction, "sohbet-1", simdi=110), 0)
self.assertIn("sohbet-1", yarim._KAYIT) # still waiting
saved = {"input": [{"type": "message", "role": "assistant", "content": [
{"type": "output_text", "text": "1. bir sayidir ve ilk siradadir. 2. iki sayidir ve cifttir. 3. uc"}]}, user("devam")]}
self.assertEqual(yarim.uygula(saved, "sohbet-1", simdi=120), 0)
self.assertNotIn("sohbet-1", yarim._KAYIT)
yarim.kaydet(self.cut(), simdi=100)
self.assertEqual(yarim.uygula({"input": [user("gec")]}, "sohbet-1", simdi=100 + yarim.OMUR_SN + 1), 0) # expired
def test_long_text_keeps_head_and_tail(self):
yarim.kaydet(self.cut(text="BAS" + "x" * 40000 + "SON"), simdi=1)
req = {"input": [user("devam")]}
yarim.uygula(req, "sohbet-1", simdi=2)
note = req["input"][-1]["content"][-1]["text"]
self.assertLess(len(note), yarim.EN_COK_KARAKTER + 600)
self.assertIn("BAS", note)
self.assertTrue(note.rstrip().endswith("</yarim_cevap>"))
self.assertIn("SON\n</yarim_cevap>", note)
if __name__ == "__main__":
unittest.main()
+127
View File
@@ -0,0 +1,127 @@
"""Yarida kesilen cevabi sonraki modele tasir (10 Ekim 2026).
Olcum: Codex kapanir ya da kullanici cevabi durdurursa o ana kadar yazilan metin sohbet gecmisine girmiyor;
sonraki tur (ayni ya da baska modelle) "onceki cevabi goremiyorum" diyor. Kopru akisi zaten goruyor:
ISTEMCI baglantiyi kapattiginda (yanit bitmeden) o ana kadarki metni bellekte tutar ve ayni sohbetin
sonraki isteginde, kullanicinin yeni mesajina kisa bir not olarak ekler. Bir kez kullanilir.
Bilerek yapilmayanlar:
- Ust kaynak kopmasinda (istemci hala bagli) kayit yok: Codex o istegi kendisi yeniden gonderir; not eklenirse
model bastan yazmak yerine ortadan devam eder ve cevabin basi kaybolur.
- Yarim metin asistan mesaji olarak eklenmez: Claude "asistan mesajiyla biten istek"i reddeder ve abonelik
yolunda kimliksiz asistan ogesi risklidir. Kullanici mesajina eklenen not her saglayicida gecerlidir.
- Diske yazilmaz; kopru yeniden baslarsa kayit gider.
"""
import json
import threading
import time
OMUR_SN = 6 * 3600
EN_COK_KAYIT = 50
EN_COK_KARAKTER = 12_000 # nota konan yarim metnin ust siniri (basi ve sonu korunur)
EN_AZ_KARAKTER = 40 # bundan kisa yarim metin tasinmaya degmez
NOT_BASI = "[Köprü notu: bir önceki cevabın yarıda kesildi ve sohbet geçmişine kaydedilmedi."
_KILIT = threading.Lock()
_KAYIT = {} # sohbet anahtari -> {"zaman", "metin", "model"}
class Izleyici:
"""Akistan gecen parcalardan asistan metnini toplar; yanitin bitip bitmedigini izler."""
def __init__(self, anahtar, model):
self.anahtar, self.model = anahtar, model
self.buf = b""
self.parcalar = []
self.bitti = False
def feed(self, chunk):
self.buf += chunk
if b"\r" in self.buf:
self.buf = self.buf.replace(b"\r\n", b"\n") # CRLF ile ayrilan SSE de izlenir
while b"\n\n" in self.buf:
blok, self.buf = self.buf.split(b"\n\n", 1)
if b"response.output_text.delta" in blok:
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
d = json.loads(satir[5:])
except ValueError:
continue
if d.get("type") == "response.output_text.delta" and isinstance(d.get("delta"), str):
self.parcalar.append(d["delta"])
elif b"response.completed" in blok or b"response.failed" in blok or b"response.incomplete" in blok:
# Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
tur = json.loads(satir[5:]).get("type")
except (ValueError, AttributeError):
continue
if tur in ("response.completed", "response.failed", "response.incomplete"):
self.bitti = True
@property
def metin(self):
return "".join(self.parcalar)
def kaydet(izleyici, simdi=None):
"""Istemci kapatti ve yanit bitmemisti: yarim metni sakla. True: saklandi."""
metin = izleyici.metin.strip()
if not izleyici.anahtar or izleyici.bitti or len(metin) < EN_AZ_KARAKTER:
return False
simdi = time.time() if simdi is None else simdi
with _KILIT:
_KAYIT[izleyici.anahtar] = {"zaman": simdi, "metin": metin, "model": izleyici.model}
for eski in sorted(_KAYIT, key=lambda k: _KAYIT[k]["zaman"])[:max(0, len(_KAYIT) - EN_COK_KAYIT)]:
del _KAYIT[eski]
return True
def _kisalt(metin):
if len(metin) <= EN_COK_KARAKTER:
return metin
bas, son = EN_COK_KARAKTER // 4, EN_COK_KARAKTER - EN_COK_KARAKTER // 4
return metin[:bas] + f"\n[... ortadan {len(metin) - bas - son} karakter çıkarıldı ...]\n" + metin[-son:]
def uygula(req, anahtar, simdi=None):
"""Ayni sohbetin sonraki isteginde yarim metni kullanicinin son mesajina not olarak ekler.
Donus: eklenen karakter sayisi (0: eklenmedi). Kayit yalniz basariyla eklenince ya da eskiyince silinir."""
if not anahtar:
return 0
simdi = time.time() if simdi is None else simdi
with _KILIT:
kayit = _KAYIT.get(anahtar)
if kayit and simdi - kayit["zaman"] > OMUR_SN:
del _KAYIT[anahtar]
kayit = None
girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi:
return 0
son = None
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
continue
son = oge
break
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"]
ham = json.dumps(girdi, ensure_ascii=False)
if json.dumps(metin[:200], ensure_ascii=False)[1:-1] in ham:
with _KILIT: # Codex bu metni gecmise kendisi yazmis: ikinci kez eklenmez
_KAYIT.pop(anahtar, None)
return 0
notu = (f"{NOT_BASI} Kesilmeden önce yazdığın metin aşağıda. Kullanıcı devam etmeni isterse baştan yazma, "
f"kaldığın yerden sürdür.]\n<yarim_cevap model=\"{kayit['model']}\">\n{_kisalt(metin)}\n</yarim_cevap>")
icerik = son.get("content")
if isinstance(icerik, str):
son["content"] = [{"type": "input_text", "text": icerik}, {"type": "input_text", "text": notu}]
elif isinstance(icerik, list):
icerik.append({"type": "input_text", "text": notu})
else:
return 0
with _KILIT:
_KAYIT.pop(anahtar, None)
return len(notu)
@@ -230,7 +230,7 @@ describe('bridge configuration and command', () => {
routes: {},
nine_router_catalog: { enabled: false },
pano_kanca_user: false,
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
})
})
+2 -2
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '6'
export const CODEX_BRIDGE_VERSION = '8'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
@@ -339,7 +339,7 @@ export function codexBridgeConfig() {
routes: {},
nine_router_catalog: { enabled: false },
pano_kanca_user: false,
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 3 }
ozet_kopru: { enabled: true, kapsam: 'hepsi', openai_modeli: 'gpt-6-luna', dongu_siniri: 8, dongu_art_arda: 3 }
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ export interface CodexLinkDeps {
// The single wait this feature owns: how long a freshly spawned bridge gets to
// answer its status endpoint before the apply gives up.
export const CODEX_BRIDGE_START_BUDGET_MS = 10_000
export const CODEX_BRIDGE_START_BUDGET_MS = 45_000
const START_POLL_INTERVAL_MS = 500
// A probe of an already-running (or absent) local bridge. Loopback answers or
// refuses at once; the bound only keeps Settings from hanging on a wedged port.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.25",
"version": "1.0.0-beta.27",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.25",
"version": "1.0.0-beta.27",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",