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
yilsemandClaude Opus 5.5 ac0fdcf355 fix(codex-link): show added models as provider/model, drop the 9R mark
ci / mobile-link-windows (push) Successful in 33s
ci / approval-gate-windows (push) Successful in 2m40s
User decision (10 Oct 2026): combos keep their own name; single models
read as short provider/model with the version kept (cc/opus-5-5,
agy/gemini-3.8-flash-high, bekci/kurnaz). No-think copies show their
real provider with a "(no-think)" suffix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:52:55 +03:00
yilsemandClaude Opus 5.5 9a54705ed5 fix(codex-link): label cx models as Codex, matching the 9Router alias
ci / mobile-link-windows (push) Successful in 37s
ci / approval-gate-windows (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:55:04 +03:00
yilsemandClaude Opus 5.5 9739222919 fix(codex-link): only reject degenerate summaries, not short correct ones
ci / mobile-link-windows (push) Successful in 36s
ci / approval-gate-windows (push) Successful in 2m54s
The size-proportional minimum (input/250, cap 4000) rejected a correct summary:
a 646,955 byte thread (225k tokens, five labelled facts in a long log) was
summarized by the subscription model in 1501-1701 characters, the bridge demanded
2587, and Codex could not continue the thread on the selected model. The floor now
only catches broken output (input/2000, cap 300). Memory loss is prevented by who
writes the summary, not by its length.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRDGW1LiKcDUZYUdKBoWYW
2026-10-09 17:51:10 +03:00
28 changed files with 3405 additions and 168 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with:
github-server-url: https://gitea.twinpay.one
- name: Mobil TLS, profil ve baslatma regresyonlari
@@ -57,7 +57,7 @@ jobs:
timeout-minutes: 40
steps:
- name: Kaynagi al
uses: actions/checkout@v4
uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
with:
github-server-url: https://gitea.twinpay.one
@@ -10,6 +10,13 @@ Codex depends on it while `openai_base_url` points here.
key read from the agent's `.env` on every catalog refresh (never written elsewhere).
- `ide_catalog.py`: which IDE models and combos are added to Codex's model list and
how they are named (`provider/model`, combos by bare name).
The public `/models` catalog supplies every OmniRoute chat model; a member does
not need a private local model allowlist. `/router/catalog` supplies only the
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
entries and internal 9Router combos are excluded. 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.
@@ -19,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.
File diff suppressed because it is too large Load Diff
+231 -22
View File
@@ -6,11 +6,13 @@ anahtar IDE'nin kendi .env dosyasindan her yenilemede okunur, bridge.json'a yazi
Ac/kapat: %LOCALAPPDATA%\\TurkServis\\AITURK-IDE\\codex-baglama.json
{"enabled": true, "kapsam": "ide"}
Dosya yoksa ACIK sayilir (kullanici karari, 9 Ekim 2026: varsayilan acik).
kapsam "ide" : IDE ayarinda tanimli modeller + adlandirilmis combolar (varsayilan)
kapsam "hepsi" : gecitteki butun sohbet modelleri (1000+; Codex listesi cok buyur)
kapsam "ide" ve "hepsi": gecidin uye katalogundaki OmniRoute sohbet modelleri.
Yerel config'teki model adlari uye katalogunu sinirlamaz.
"""
import json, os, re, urllib.request
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))
@@ -84,11 +86,18 @@ def ide_yapilandirma(agent=AGENT):
return base.rstrip("/"), varsayilan, adlar
AITURK_EKI = " (AITURK)"
# Gecidin kisa saglayici kodu -> kullanicinin gormek istedigi kod (10 Ekim 2026: "agy/gemini").
_SAGLAYICI_KISA = {"ag": "agy"}
# Saglayici kodu zaten soyledigi icin model adinin basinda tekrar edilmeyen uretici adi ("cc/opus-5-5").
_TEKRAR = ("claude-",)
DUSUNMESIZ = "no-think-"
def kisa_ad(ad):
"""Codex secicisinde gorunecek ad: yalniz kullanicinin ekledigi saglayici + model adi
(kullanici istegi, 9 Ekim 2026). "OmniRoute · " / "9Router · " gibi gecit onekleri ve aradaki
saglayici halkalari gosterilmez: "nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b",
"OmniRoute · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash", combo adi oldugu gibi kalir."""
"""Gecit onekleri ("OmniRoute · ", "9Router · ", "9R · ") ve aradaki saglayici halkalari atilir:
"nano/qwen/qwen3.8-27b" -> "nano/qwen3.8-27b", "9R · ag/gemini-3.8-flash" -> "ag/gemini-3.8-flash",
combo adi oldugu gibi kalir. Kullanici karari (10 Ekim 2026): "9R" yazilmaz."""
ad = (ad or "").split(" · ")[-1].strip()
parcalar = [p for p in ad.split("/") if p]
if len(parcalar) <= 2:
@@ -96,10 +105,41 @@ def kisa_ad(ad):
return parcalar[0] + "/" + parcalar[-1]
def adlandir(girdiler, dolu=()):
def gorunen_ad(kimlik, ad, tur="model"):
"""Secicide gorunen ad (kullanici karari, 10 Ekim 2026): combo yalniz kendi adiyla; tek model
"saglayici/model" kalibinda, kisa ve surumlu: "9R · cc/claude-opus-5-5" -> "cc/opus-5-5",
"9R · ag/gemini-3.8-flash-high" -> "agy/gemini-3.8-flash-high", "Agnes 2.0 Flash" (agnes-agnes-2.0-flash)
-> "agnes/agnes-2.0-flash", "kurnaz" (bekci-kurnaz) -> "bekci/kurnaz". Dusunmesiz kopya asil
saglayicisiyla gorunur: "no-think/claude-opus-5-5" -> "cc/opus-5-5 (no-think)"."""
yalin = kisa_ad(ad)
if tur == "combo":
return yalin
kim, ek = kimlik or "", ""
if kim.startswith(DUSUNMESIZ):
kim, ek = kim[len(DUSUNMESIZ):], " (no-think)"
if kim.startswith("9router-"):
kim = kim[len("9router-"):]
saglayici, _, model = yalin.partition("/")
if not model or ek:
kod, _, kalan = kim.partition("-")
if not kod or not kalan:
return yalin
saglayici, model = kod, model or kalan
model = model.replace(" ", "-")
for tekrar in _TEKRAR:
if model.lower().startswith(tekrar) and len(model) > len(tekrar):
model = model[len(tekrar):]
return f"{_SAGLAYICI_KISA.get(saglayici.lower(), saglayici)}/{model}{ek}"
def adlandir(girdiler, dolu=(), turler=None):
"""[(kimlik, tam_ad)] -> {kimlik: gorunen_ad}. Kisa adlar cakisirsa aradaki halkalar geri konur;
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar."""
kisa = {k: kisa_ad(a) for k, a in girdiler}
yine cakisirsa kimligin sonu eklenir. `dolu`: baska kaynaktan gelen, zaten kullanilan adlar.
`turler` ({kimlik: "combo"|"model"}) verilirse adlar gorunen_ad kuraliyla yazilir."""
if turler is None:
kisa = {k: kisa_ad(a) for k, a in girdiler}
else:
kisa = {k: gorunen_ad(k, a, turler.get(k, "model")) for k, a in girdiler}
tam = {k: (a or "").split(" · ")[-1].strip() for k, a in girdiler}
sayac = {}
for v in list(kisa.values()) + list(dolu):
@@ -108,16 +148,20 @@ def adlandir(girdiler, dolu=()):
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
def sec(data, tanimli, kapsam="ide"):
"""Gecit katalogundan Codex'e eklenecek girdiler: [{'id','ad'}], sirali ve tekrarsiz.
def sec(data, tanimli=(), kapsam="ide", turler=None):
"""Gecidin uye katalogundan girdiler: [{'id','ad','kind'}], sirali ve tekrarsiz.
Yalniz sohbet modelleri; resim/ses/video/embedding uretenler eklenmez."""
tanimli = {a.lower() for a in tanimli if a}
turler = turler or {}
secilen, gorulen = [], set()
for m in data:
mid = m.get("id") or ""
@@ -125,19 +169,160 @@ def sec(data, tanimli, kapsam="ide"):
if not mid or mid in gorulen or "chat" not in (ts.get("capabilities") or ["chat"]):
continue
gorunen = ts.get("display_name") or mid
kisa = gorunen.split(" · ")[-1].strip()
combo = "/" not in kisa # saglayici/model bicimi degil: adlandirilmis combo ya da takma ad
if kapsam != "hepsi" and not combo and kisa.lower() not in tanimli and mid.lower() not in tanimli:
kind = turler.get(mid) or ts.get("kind") or "model"
# Tek uye yolu: 9Router -> OmniRoute -> TurkServis -> Codex.
# Dogrudan 9Router satirlari ayni modelin ikinci kopyasi olarak eklenmez.
if ts.get("source") != "omniroute":
continue
# Combo adi biciminden tahmin edilmez; duz adli Agnes/Bekci modelleri de vardir.
if kind == "combo" and mid.lower().startswith("9router-"):
continue
gorulen.add(mid)
secilen.append({"id": mid, "ad": gorunen, "sira": ts.get("order") or 0})
secilen.append({"id": mid, "ad": gorunen, "kind": kind, "sira": ts.get("order") or 0,
"yol": str(m.get("description") or "").split(" · ")[-1].strip()})
secilen.sort(key=lambda s: (s["sira"], s["id"]))
return [{"id": s["id"], "ad": s["ad"]} for s in secilen]
# Gecit ayni adin ikinci yoluna "ad (2)" der; birincisi listede yoksa sayac anlamsizdir.
adlar = {s["ad"].split(" · ")[-1].strip().lower() for s in secilen}
for s in secilen:
taban = re.sub(r" \(\d+\)$", "", s["ad"])
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
adlar.add(taban.split(" · ")[-1].strip().lower())
s["ad"] = taban
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
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.
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
return re.sub(r"[^0-9a-z]+", "", (ad or "").casefold())
ozgun_adlar = {yalin(m.get("display_name") or m.get("slug", "")) for m in ozgunler}
def anahtar(m):
slug = m.get("slug", "")
bilgi = bilgiler.get(slug, {})
ad = kisa_ad(m.get("display_name") or slug)
if bilgi.get("kind") == "combo":
return (0, ad.casefold(), "", slug)
if slug.startswith("ide/"):
mid = slug[len("ide/"):]
if mid.startswith("9router-"):
mid = mid[len("9router-"):]
saglayici, _, model = ad.partition("/")
if not model:
saglayici, _, _ = mid.partition("-")
model = ad
else:
saglayici, model = "OpenAI", ad
return (1, saglayici.casefold(), model.casefold(), slug)
ek_modeller.sort(key=anahtar)
taban = max([m.get("priority") or 0 for m in ozgunler], default=-1) + 1
for n, model in enumerate(ek_modeller):
model["priority"] = taban + n
ad = model.get("display_name") or model.get("slug", "")
if yalin(ad) in ozgun_adlar:
model["display_name"] = f"{ad}{AITURK_EKI}"
modeller[:] = ozgunler + ek_modeller
# ---- Secicide model adinin altindaki satir ----
# Telefonda bu satir tek satirdir ve yaklasik 30 karakterde kesilir (kullanici ekran goruntusu,
# 10 Ekim 2026); en ayirt edici bilgi basa yazilir, ad tekrar edilmez.
ACIKLAMA_SINIRI = 30
KUCUK_PENCERE = 100_000 # bunun altindaki pencere alt satirda yazilir
_AILE = (("opus", "opus"), ("sonnet", "sonnet"), ("haiku", "haiku"), ("astra", "astra"), ("terra", "terra"),
("luna", "luna"), ("sol", "sol"), ("gemini", "gemini"), ("qwen", "qwen"), ("glm", "glm"),
("abliterated", "abliterated"), ("deepseek", "deepseek"), ("llama", "llama"), ("gemma", "gemma"))
_SAGLAYICI = {"cc": "Claude", "cx": "Codex", "ag": "Antigravity", "agy": "Antigravity", "nano": "NanoGPT", "hf": "HuggingFace",
"huggingface": "HuggingFace", "agnes": "Agnes", "bekci": "Bekçi"}
_SANSURSUZ = re.compile(r"uncensored|abliterat|obliterat|derestrict|heretic|unshackled", re.I)
def kisa_model(ad):
""""cc/claude-opus-5-5" -> "opus", "nano/z-ai/glm-5.3-flash-uncensored" -> "glm"; bilinmeyen ad "?"."""
if not ad:
return "?"
son = ad.split("/")[-1].split(":")[0].lower()
parcalar = [p for p in re.split(r"[^a-z]+", son) if p]
for anahtar, kisa in _AILE:
if anahtar in parcalar:
return kisa
return (parcalar[0] if parcalar else son) or "?"
def _yapraklar(dugum, derinlik=0):
"""Sirali (priority) zinciri duz model listesine acar; ic takim kendi uyeleriyle yer alir."""
if not isinstance(dugum, dict) or derinlik > 6:
return ["?"]
if dugum.get("kind") != "combo":
return [kisa_model(dugum.get("model"))]
cikti = []
for uye in dugum.get("members") or []:
cikti.extend(_yapraklar(uye, derinlik + 1))
return cikti or ["?"]
def _sigdir(onek, adlar, ayrac):
"""Sinira sigdigi kadar ad yazar, kalani "+N" ile sayar."""
tekil = [a for i, a in enumerate(adlar) if i == 0 or a != adlar[i - 1]]
for adet in range(len(tekil), 0, -1):
kalan = len(tekil) - adet
metin = onek + ayrac.join(tekil[:adet]) + (f" +{kalan}" if kalan else "")
if len(metin) <= ACIKLAMA_SINIRI or adet == 1:
return metin
return onek.rstrip(": ")
def aciklama(kimlik, ad, tur, kayit=None, bilgi=None):
"""Model adinin altinda gorunecek kisa satir. kayit: gecidin /router/catalog girdisi (yoksa None).
Takim -> uyeleri; sirali yedek -> sirasi; tek model -> saglayicisi ve adindan okunan ozellikleri.
bilgi (pencere.coz sonucu): Codex'te isi etkileyen iki uyari one yazilir: kucuk baglam ("32K")
ve arac cagiramayan model ("araçsız")."""
if tur == "combo":
topoloji = (kayit or {}).get("topology") or {}
uyeler = topoloji.get("members") or []
if topoloji.get("strategy") == "round-robin":
return f"Dönüşümlü: {len(uyeler)} üye"
takim = topoloji if topoloji.get("strategy") == "fusion" else None
if takim is None and uyeler and isinstance(uyeler[0], dict) and uyeler[0].get("strategy") == "fusion":
takim = uyeler[0]
if takim is not None:
return _sigdir("Takım: ", [kisa_model(u.get("model")) for u in takim.get("members") or []], "+")
if uyeler:
return _sigdir("Sıralı: ", _yapraklar(topoloji), ">")
return "Takım"
kisa = gorunen_ad(kimlik, ad, "model")
kod = kisa.split("/")[0].lower() if "/" in kisa else (kimlik or "").split("-")[0].lower()
parcalar = [_SAGLAYICI.get(kod, kod) or "Tek model"]
if bilgi and bilgi.get("pencere") and bilgi["pencere"] < KUCUK_PENCERE:
parcalar.append(pencere.kisa(bilgi["pencere"]))
if bilgi and bilgi.get("arac") is False:
parcalar.append("araçsız")
if _SANSURSUZ.search(kisa) or _SANSURSUZ.search(kimlik or ""):
parcalar.append("sansürsüz")
if "thinking" in (kisa + " " + (kimlik or "")).lower():
parcalar.append("düşünen")
if len(parcalar) == 1:
parcalar.insert(0, "Tek model")
while len(parcalar) > 1 and len(" · ".join(parcalar)) > ACIKLAMA_SINIRI:
parcalar.pop() # sona yazilan en az onemli ozellik duser; kelime ortasindan kesilmez
return " · ".join(parcalar)[:ACIKLAMA_SINIRI]
def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
"""Donus: (durum, base_url, anahtar, girdiler). durum: 'kapali' | 'anahtar-yok' | 'tamam'.
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", "", "", []
@@ -146,5 +331,29 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30):
return "anahtar-yok", "", "", []
base, varsayilan, adlar = ide_yapilandirma()
req = urllib.request.Request(base + "/models", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
data = json.load(urllib.request.urlopen(req, timeout=timeout)).get("data", [])
return "tamam", base, anahtar, sec(data, adlar + [varsayilan], ayar["kapsam"])
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.load(resp).get("data")
if not isinstance(data, list) or not data:
raise ValueError("Gecit bos veya gecersiz model katalogu dondurdu")
req = urllib.request.Request(base + "/router/catalog", headers={"Authorization": f"Bearer {anahtar}", "User-Agent": ua})
with urllib.request.urlopen(req, timeout=timeout) as resp:
metadata = json.load(resp).get("models")
if not isinstance(metadata, list) or not metadata:
raise ValueError("Gecit model turlerini dondurmedi")
# Bu ucta uye listesinde olmayan ic kayitlar da bulunur. Yalniz tur bilgisi alinir;
# modele erisim listesi her zaman /models yanitidir.
turler = {m["id"]: m["kind"] for m in metadata if m.get("id") and m.get("kind") in ("combo", "model")}
girdiler = sec(data, adlar + [varsayilan], ayar["kapsam"], turler)
if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
ogr = pencere.ogrenilen(home)
aracsiz = pencere.aracsizlar(home)
for g in girdiler:
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
bilgi["arac"], bilgi["kaynak"])
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
g["aciklama"] = aciklama(g["id"], g["ad"], g["kind"], kayitlar.get(g["id"]), bilgi)
return "tamam", base, anahtar, girdiler
@@ -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()
+54 -7
View File
@@ -45,6 +45,22 @@ OKUNAMADI_NOTU = ("(Kopru notu: Bu noktadan onceki konusma, onceki modelin sifre
"aktarilamadi. Onceki kararlari bilmiyorsan uydurma; kullaniciya sor.)")
KISA_MESAJI = ("AstraBridge: Sohbet ozeti cok kisa cikti (%d karakter, en az %d bekleniyordu) ve gecmisin yerine "
"konmadi; mesajlariniz sohbette duruyor. Tekrar deneyin ya da daha guclu bir model secin.")
def en_az_karakter(girdi_bayt, ayar=None):
"""Bu buyuklukteki bir konusmanin ozeti en az kac karakter olmali.
Yalniz bozuk/bos sayilacak kadar kisa ozeti yakalar; ozetin yeterliligini boyuttan cikarmaz.
Olcum (10 Ekim 2026, 646.955 baytlik gunluk + bes olgu): guclu modelin dogru ozeti 1500-1700
karakterdi; boyuta oranli yuksek esik (2587) bu dogru ozeti reddedip sohbeti kilitledi.
Hafiza kaybinin asil onlemi ozeti guclu modele yazdirmaktir (bridge.py _ozet_openai)."""
ayar = ayar or {}
tavan = int(ayar.get("en_az_tavan", 300))
oran = max(1, int(ayar.get("en_az_oran", 2000))) # her 2000 bayt girdi icin 1 karakter
return max(0, min(tavan, int(girdi_bayt) // oran))
def tetik_mi(req):
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
ogeler = req.get("input")
@@ -129,9 +145,11 @@ class OzetAkisi:
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
ic: once calisan cevirici (SseRewriter). Metin gelmezse compaction ogesi uretmez (Codex hatayi gosterir)."""
def __init__(self, ic=None, tohum=""):
def __init__(self, ic=None, tohum="", en_az=0):
self.ic = ic
self.tohum = tohum
self.en_az = en_az # bundan kisa ozet kabul edilmez (bkz. en_az_karakter)
self.kisa = False
self.buf = b""
self.parcalar = {} # item_id -> delta metni
self.bitenler = [] # tamamlanan mesaj metinleri
@@ -171,6 +189,13 @@ class OzetAkisi:
self.bitti = True
if not self.metin:
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
if len(self.metin) < self.en_az:
# Anormal kisa ozet gecmisin yerine konmaz: Codex hatayi gosterir, sohbet oldugu gibi kalir.
self.kisa = True
yanit = dict(d.get("response") or {})
yanit["status"] = "failed"; yanit["output"] = []
yanit["error"] = {"code": "astrabridge_ozet_kisa", "message": KISA_MESAJI % (len(self.metin), self.en_az)}
return _olay({"type": "response.failed", "response": yanit})
oge = ozet_ogesi(self.metin, self.tohum)
yanit = dict(d.get("response") or {})
yanit["output"] = [oge]
@@ -180,6 +205,14 @@ class OzetAkisi:
return _olay(d)
return b""
def sonuc_notu(self):
"""Gunluk satiri icin sonuc (icerik yazilmaz)."""
if not self.metin:
return "ozet URETILEMEDI"
if self.kisa:
return f"ozet REDDEDILDI: {len(self.metin)} karakter, en az {self.en_az} bekleniyordu; gecmis korunuyor"
return f"ozet uretildi, {len(self.metin)} karakter"
def feed(self, chunk):
if self.ic:
chunk = self.ic.feed(chunk)
@@ -233,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 ----
@@ -248,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()
@@ -2,8 +2,8 @@ import json, os, tempfile, unittest
import ide_catalog as c
def girdi(mid, ad, yetenek=("chat",), sira=0):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira}}
def girdi(mid, ad, yetenek=("chat",), sira=0, kaynak="omniroute", kind="model"):
return {"id": mid, "turkservis": {"display_name": ad, "capabilities": list(yetenek), "order": sira, "source": kaynak, "kind": kind}}
KATALOG = [
@@ -11,8 +11,8 @@ KATALOG = [
girdi("9router-cc-opus", "OmniRoute · cc/claude-opus-5-5", sira=2),
girdi("9router-nano-x", "OmniRoute · nano/x", sira=1),
girdi("gorsel", "OmniRoute · gpt-image-1", ("image",)),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8),
girdi("9router-Takim-Agir", "OmniRoute · Takim-Agir", sira=9, kind="combo"),
girdi("9router-Takim-Agir-6e65dba5", "OmniRoute · Takim-Agir", sira=8, kind="combo"),
girdi("Ajan-Arac", "OmniRoute · Ajan-Arac (tekrar)"),
]
@@ -52,14 +52,39 @@ class Yapilandirma(unittest.TestCase):
class Secim(unittest.TestCase):
def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Tanimli model + adlandirilmis combolar; tanimsiz saglayici modeli ve gorsel modeli yok.
self.assertEqual([x["id"] for x in s], ["9router-cc-opus", "Ajan-Arac", "9router-Takim-Agir-6e65dba5", "9router-Takim-Agir"])
# Uye katalogu yerel config'teki adlarla sinirlanmaz.
self.assertEqual([x["id"] for x in s], ["9router-nano-x", "9router-cc-opus", "Ajan-Arac"])
def test_9router_combolari_gosterilmez(self):
katalog = [girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=1),
girdi("9router-Takim-Agir", "Takim-Agir (2)", sira=2, kind="combo"),
girdi("Takim-Agir", "Takim-Agir (3)", sira=3),
girdi("Ajan-Arac", "Ajan-Arac", sira=4),
girdi("9router-nano-x", "nano/x", kaynak="9router", sira=5)]
s = c.sec(katalog, ["nano/x"], "ide")
# 9Router combosu ve gecidin 9router saglayicisindaki kopyasi yok; OmniRoute'un kendi combosu
# sayacsiz adiyla kalir. Dogrudan 9Router tekilleri de eklenmez.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("Takim-Agir", "Takim-Agir"), ("Ajan-Arac", "Ajan-Arac")])
def test_9router_modelleri_yalniz_omniroute_uzerinden_eklenir(self):
katalog = [girdi("9router-nano-x-aaaa1111", "nano/x", kaynak="9router", sira=1),
girdi("9router-nano-x", "nano/x (2)", sira=2),
girdi("9router-ocg-y-bbbb2222", "ocg/y", kaynak="9router", sira=3),
girdi("vag-z", "vag/z", sira=4),
girdi("9router-ses", "nano/ses", ("tts",), kaynak="9router", sira=5),
girdi("9router-Takim-Agir-6e65dba5", "Takim-Agir", kaynak="9router", sira=6)]
s = c.sec(katalog, [], "ide")
# Yerel model tanimi gerekmez; iki kaynak yerine yalniz OmniRoute yolu gelir.
self.assertEqual([(x["id"], x["ad"]) for x in s],
[("9router-nano-x", "nano/x"), ("vag-z", "vag/z")])
def test_gorunen_ad_yalniz_saglayici_ve_model(self):
for tam, beklenen in [("OmniRoute · ag/gemini-3.8-flash", "ag/gemini-3.8-flash"),
("nano/qwen/qwen3.8-27b-obliterated:thinking", "nano/qwen3.8-27b-obliterated:thinking"),
("9Router · Nous-Research/inclusionai/ling-3.1-flash", "Nous-Research/ling-3.1-flash"),
("cf/@cf/qwen/qwq-32b", "cf/qwq-32b"),
("9R · nano/qwen/qwen3.8-27b", "nano/qwen3.8-27b"), ("9R · ag/gemini", "ag/gemini"),
("OmniRoute · Ajan-Arac", "Ajan-Arac"), ("cc/claude-opus-5-5", "cc/claude-opus-5-5")]:
self.assertEqual(c.kisa_ad(tam), beklenen, tam)
@@ -74,7 +99,7 @@ class Secim(unittest.TestCase):
def test_hepsi_kapsami_yalniz_sohbet(self):
s = c.sec(KATALOG, [], "hepsi")
self.assertEqual(len(s), 5)
self.assertEqual(len(s), 3) # iki 9Router combosu hicbir kapsamda gosterilmez
self.assertNotIn("gorsel", [x["id"] for x in s])
@@ -0,0 +1,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,141 @@
"""The shipped IDE must use the same public OmniRoute catalog as every member."""
import io
import json
import unittest
from unittest.mock import patch
import ide_catalog as c
def row(mid, name, source="omniroute", capabilities=("chat",)):
return {"id": mid, "turkservis": {"display_name": name, "source": source,
"capabilities": list(capabilities)}}
class MemberCatalog(unittest.TestCase):
def test_metadata_classifies_only_public_models(self):
public = [row("plain", "Plain"), row("team", "Team")]
metadata = [{"id": "plain", "kind": "model"}, {"id": "team", "kind": "combo"},
{"id": "internal", "kind": "combo"}]
replies = [io.BytesIO(json.dumps({"data": public}).encode()),
io.BytesIO(json.dumps({"models": metadata}).encode())]
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
patch.object(c, "anahtar_oku", return_value="test-key"), \
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
patch.object(c.urllib.request, "urlopen", side_effect=replies):
status, _, _, selected = c.yukle()
self.assertEqual(status, "tamam")
self.assertEqual({m["id"]: m["kind"] for m in selected}, {"plain": "model", "team": "combo"})
def test_empty_catalog_is_a_refresh_failure(self):
with patch.object(c, "ayar_oku", return_value={"enabled": True, "kapsam": "ide"}), \
patch.object(c, "anahtar_oku", return_value="test-key"), \
patch.object(c, "ide_yapilandirma", return_value=("https://example.test/v1", "", [])), \
patch.object(c.urllib.request, "urlopen", return_value=io.BytesIO(b'{"data": []}')):
with self.assertRaises(ValueError):
c.yukle()
def test_public_omniroute_models_do_not_need_private_local_configuration(self):
rows = [row("9router-nano-x", "9R · nano/x"), row("agnes-flat", "Agnes Flat"),
row("Ajan-Arac", "Ajan-Arac"), row("image", "image", capabilities=("image",))]
self.assertEqual({m["id"] for m in c.sec(rows, [])},
{"9router-nano-x", "agnes-flat", "Ajan-Arac"})
def test_direct_route_is_never_added_beside_omniroute(self):
rows = [row("9router-nano-x-direct", "9R · nano/x", "9router"),
row("9router-nano-x", "9R · nano/x")]
self.assertEqual([m["id"] for m in c.sec(rows, ["nano/x"], "hepsi")], ["9router-nano-x"])
def test_model_order_uses_combo_metadata_and_preserves_every_model(self):
models = [{"slug": "gpt-z", "display_name": "GPT Z", "priority": 1},
{"slug": "ide/bekci-kurnaz", "display_name": "kurnaz", "priority": 2},
{"slug": "ide/Z-Team", "display_name": "Z-Team", "priority": 5},
{"slug": "ide/9router-cc-opus", "display_name": "9R · cc/opus", "priority": 3},
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 4}]
extras = [{"slug": m["slug"], "kind": "combo" if m["slug"].endswith("Team") else "model"}
for m in models if m["slug"].startswith("ide/")]
c.sirala_codex(models, extras)
self.assertEqual([m["slug"] for m in models],
["gpt-z", "ide/A-Team", "ide/Z-Team", "ide/bekci-kurnaz", "ide/9router-cc-opus"])
self.assertEqual([m["priority"] for m in models], [1, 2, 3, 4, 5])
def test_subscription_models_keep_their_own_order_priority_and_name(self):
models = [{"slug": "ide/GPT-6-Astra", "display_name": "GPT-6-Astra", "priority": 3},
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2},
{"slug": "ide/A-Team", "display_name": "A-Team", "priority": 0},
{"slug": "gpt-6.1-sol", "display_name": "GPT-6.1-Sol", "priority": 1},
{"slug": "codex-auto-review", "display_name": "Codex Auto Review", "priority": 43}]
models.append({"slug": "ide/GPT-6-1-Sol", "display_name": "GPT-6-1-Sol", "priority": 2})
extras = [{"slug": "ide/GPT-6-Astra", "kind": "combo"}, {"slug": "ide/A-Team", "kind": "combo"},
{"slug": "ide/GPT-6-1-Sol", "kind": "combo"}]
originals = [dict(m) for m in models if not m["slug"].startswith("ide/")]
c.sirala_codex(models, extras)
self.assertEqual([m for m in models if not m["slug"].startswith("ide/")], originals)
self.assertEqual([m["slug"] for m in models],
["gpt-6-astra", "gpt-6.1-sol", "codex-auto-review", "ide/A-Team", "ide/GPT-6-1-Sol",
"ide/GPT-6-Astra"])
self.assertEqual([m["priority"] for m in models[3:]], [44, 45, 46])
self.assertEqual([m["display_name"] for m in models[4:]], ["GPT-6-1-Sol (AITURK)", "GPT-6-Astra (AITURK)"])
def test_description_line_tells_what_the_entry_is_and_fits_a_phone_row(self):
yedek = {"kind": "combo", "model": "Yedek", "strategy": "priority", "members": [
{"kind": "model", "model": "nano/z-ai/glm-5.3-flash-uncensored"},
{"kind": "model", "model": "nano/abliteration-ai/abliterated-model-large-v2"}]}
takim = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "combo", "model": "Takim-Hizli", "strategy": "fusion", "members": [
{"kind": "model", "model": "ag/gemini-3.8-flash-high"}, {"kind": "model", "model": "cc/claude-sonnet-5-5"}]},
yedek]}}
self.assertEqual(c.aciklama("Takim-Hizli", "Takim-Hizli", "combo", takim), "Takım: gemini+sonnet")
sirali = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": "cx/gpt-6-astra"}, {"kind": "model", "model": "cc/claude-opus-5-5"},
{"kind": "model", "model": "cc/claude-sonnet-5-5"}, {"kind": "model", "model": "ag/gemini-3.8-flash-high"}]}}
self.assertEqual(c.aciklama("Ajan-Arac", "Ajan-Arac", "combo", sirali), "Sıralı: astra>opus>sonnet +1")
# An unresolved first member stays visible instead of being hidden behind the fallback.
eksik = {"topology": {"kind": "combo", "strategy": "priority", "members": [{"kind": "combo", "model": None}, yedek]}}
self.assertEqual(c.aciklama("GPT-6-Sol", "GPT-6-Sol", "combo", eksik), "Sıralı: ?>glm>abliterated")
donen = {"topology": {"kind": "combo", "strategy": "round-robin", "members": [yedek, yedek, yedek]}}
self.assertEqual(c.aciklama("Takim-Sira", "Takim-Sira", "combo", donen), "Dönüşümlü: 3 üye")
self.assertEqual(c.aciklama("X", "X", "combo", None), "Takım")
self.assertEqual(c.aciklama("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"), "Tek model · Claude")
self.assertEqual(c.aciklama("9router-nano-qwen-qwen3.8-27b-uncensored-thinking",
"9R · nano/qwen3.8-27b-uncensored:thinking", "model"),
"NanoGPT · sansürsüz · düşünen")
self.assertEqual(c.aciklama("bekci-kurnaz", "kurnaz", "model"), "Tek model · Bekçi")
# 9Router aliases: cc = Claude Code, cx = OpenAI Codex, ag = Antigravity.
self.assertEqual(c.aciklama("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"), "Tek model · Codex")
self.assertEqual(c.aciklama("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"), "Tek model · Antigravity")
uzun = {"topology": {"kind": "combo", "strategy": "priority", "members": [
{"kind": "model", "model": f"x/model{chr(97 + i) * 9}"} for i in range(8)]}}
self.assertLessEqual(len(c.aciklama("U", "U", "combo", uzun)), c.ACIKLAMA_SINIRI)
for ad in ("Takim-Hizli", "9R · cc/claude-opus-5-5"):
self.assertNotIn(ad, c.aciklama(ad, ad, "combo" if "Takim" in ad else "model", takim))
def test_picker_names_are_combo_name_or_short_provider_slash_model(self):
# User decision, 10 Oct 2026: no "9R" mark; combos by name; single models as provider/model.
rows = [("Takim-Hizli", "Takim-Hizli", "combo"),
("9router-cc-claude-opus-5-5", "9R · cc/claude-opus-5-5", "model"),
("9router-cc-claude-opus-4-8", "9R · cc/claude-opus-4-8", "model"),
("9router-cx-gpt-6-astra", "9R · cx/gpt-6-astra", "model"),
("9router-ag-gemini-3.8-flash-high", "9R · ag/gemini-3.8-flash-high", "model"),
("9router-nano-z-ai-glm-5.3-flash-uncensored", "9R · nano/glm-5.3-flash-uncensored", "model"),
("agnes-agnes-2.0-flash", "Agnes 2.0 Flash", "model"),
("agnes-agnes-2.0-flash-high", "agnes-2.0-flash-high", "model"),
("bekci-bekci", "bekci", "model"), ("bekci-kurnaz", "kurnaz", "model"),
("hf-deepseek-ai-DeepSeek-V3", "hf/DeepSeek V3", "model"),
("no-think-9router-cc-claude-opus-5-5", "no-think/claude-opus-5-5", "model"),
("no-think-9router-ag-claude-opus-4-6-thinking", "no-think/claude-opus-4-6-thinking", "model")]
adlar = c.adlandir([(k, a) for k, a, _ in rows], turler={k: t for k, _, t in rows})
self.assertEqual([adlar[k] for k, _, _ in rows],
["Takim-Hizli", "cc/opus-5-5", "cc/opus-4-8", "cx/gpt-6-astra", "agy/gemini-3.8-flash-high",
"nano/glm-5.3-flash-uncensored", "agnes/agnes-2.0-flash", "agnes/agnes-2.0-flash-high",
"bekci/bekci", "bekci/kurnaz", "hf/DeepSeek-V3", "cc/opus-5-5 (no-think)",
"agy/opus-4-6-thinking (no-think)"])
self.assertFalse([a for a in adlar.values() if "9R" in a])
# Added models sort after the combos by provider then model.
models = [{"slug": "ide/" + k, "display_name": adlar[k], "priority": 0} for k, _, _ in rows]
c.sirala_codex(models, [{"slug": "ide/" + k, "kind": t} for k, _, t in rows])
gorunen = [m["display_name"] for m in models]
self.assertEqual(gorunen[0], "Takim-Hizli")
self.assertEqual(gorunen[1:], sorted(gorunen[1:], key=lambda a: tuple(p.casefold() for p in a.partition("/")[::2])))
if __name__ == "__main__":
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)
@@ -62,6 +63,27 @@ class ModelSwitch(unittest.TestCase):
self.assertEqual(rewritten["item"]["type"], "custom_tool_call")
self.assertEqual(rewritten["item"]["input"], "remember-4827")
self.assertEqual(rewritten["item"]["call_id"], "call_4827")
self.assertEqual(rewritten["item"]["id"], "ctc_chatcmpl-probe")
def test_custom_stream_ids_are_valid_before_history_is_saved(self):
# Real failure after disabling the bridge and returning to OpenAI:
# input[32].id fc_call_... was saved as a custom_tool_call by Codex.
item = {"type": "function_call", "id": "fc_call_3f427ecc539d4c779c1252077701216f",
"call_id": "call_original", "name": "probe",
"arguments": json.dumps({"input": "keep this exact tool input"})}
adapter = env["SseRewriter"]({"probe"})
converted = []
for kind in ("response.output_item.added", "response.output_item.done"):
converted.append(adapter._fix({"type": kind, "item": dict(item)})["item"])
final = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})
converted.append(final["response"]["output"][0])
for result in converted:
self.assertEqual(result["type"], "custom_tool_call")
self.assertEqual(result["id"], "ctc_call_3f427ecc539d4c779c1252077701216f")
self.assertEqual(result["call_id"], "call_original")
self.assertEqual(result["input"], "keep this exact tool input")
history = {"input": converted}
self.assertEqual(env["fix_tool_ids"](history), 0)
if __name__ == "__main__":
@@ -109,6 +109,35 @@ class Akis(unittest.TestCase):
a = o.OzetAkisi()
self.assertEqual(o.ozet_metni(coz(a.feed(ham))[1]["item"]), "ozet metni")
def test_anormal_kisa_ozet_gecmisin_yerine_konmaz(self):
# Esik yalniz bozuk sayilacak kadar kisa ozeti yakalar. 10 Ekim 2026 olcumu: 646.955 baytlik
# sohbetin dogru ozeti 1501-1701 karakterdi ve boyuta oranli eski esik onu reddedip sohbeti kilitledi.
self.assertEqual(o.en_az_karakter(1018151), 300)
self.assertEqual(o.en_az_karakter(646955), 300)
self.assertEqual(o.en_az_karakter(100000), 50)
self.assertEqual(o.en_az_karakter(0), 0)
self.assertEqual(o.en_az_karakter(1018151, {"en_az_tavan": 1500, "en_az_oran": 250}), 1500)
for gercek_ozet in (1501, 1648, 1701):
self.assertGreater(gercek_ozet, o.en_az_karakter(646955))
ham = sse({"type": "response.created", "response": {"id": "r1"}},
{"type": "response.output_item.done", "item": self._mesaj("k" * 120)},
{"type": "response.completed", "response": {"id": "r1", "status": "completed"}})
a = o.OzetAkisi(tohum="7", en_az=o.en_az_karakter(1018151))
olaylar = coz(a.feed(ham) + a.flush())
self.assertEqual([e["type"] for e in olaylar], ["response.created", "response.failed"])
self.assertNotIn("compaction", json.dumps(olaylar))
self.assertEqual(olaylar[1]["response"]["error"]["code"], "astrabridge_ozet_kisa")
self.assertTrue(a.kisa)
self.assertIn("REDDEDILDI", a.sonuc_notu())
# Yeterli uzunlukta ozet ayni esikle kabul edilir; kucuk sohbetin kisa ozeti de reddedilmez.
for metin, esik in (("u" * 1501, o.en_az_karakter(646955)), ("kisa ama dogru ozet", o.en_az_karakter(3000))):
b = o.OzetAkisi(tohum="8", en_az=esik)
ham2 = sse({"type": "response.output_item.done", "item": self._mesaj(metin)},
{"type": "response.completed", "response": {"id": "r2"}})
cikti = coz(b.feed(ham2))
self.assertEqual(o.ozet_metni(cikti[0]["item"]), metin)
self.assertFalse(b.kisa)
def test_metin_yoksa_compaction_uretilmez_hata_gorunur(self):
ham = sse({"type": "response.created", "response": {}},
{"type": "response.output_item.done", "item": {"type": "function_call", "name": "x"}},
@@ -133,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 -3
View File
@@ -27,10 +27,9 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '2'
export const CODEX_BRIDGE_VERSION = '8'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_BACKUP_SUFFIX = '.aiturk-yedek'
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
export const CODEX_LINK_RUN_VALUE = 'AITURK-IDE-Codex-Baglama'
export const CODEX_LINK_LAUNCHER_NAME = 'baslat.pyw'
@@ -340,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 }
}
}
+3 -21
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link'
import {
CODEX_BRIDGE_VERSION,
CODEX_LINK_LINE,
CODEX_LINK_REFRESH_URL,
CODEX_LINK_STATUS_URL,
@@ -21,8 +22,7 @@ const PATHS: CodexLinkPaths = {
}
const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json`
const BACKUP = `${PATHS.codexConfig}.aiturk-yedek`
const STATUS_OK = { ok: true, surum: '2', ide_durum: 'tamam', ide_model: 7, ozet: true }
const STATUS_OK = { ok: true, surum: CODEX_BRIDGE_VERSION, ide_durum: 'tamam', ide_model: 7, ozet: true }
const USER_CONFIG = 'model = "gpt-6"\r\n\r\n[features]\r\nweb = true\r\n'
interface World {
@@ -73,12 +73,6 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
world.events.push(`write ${file}`)
world.files.set(file, text)
},
backupOnce: (source, target) => {
if (!world.files.has(target)) {
world.events.push(`backup ${target}`)
world.files.set(target, world.files.get(source) ?? '')
}
},
spawnBridge: (command, args, env) => {
world.events.push(`spawn ${command} ${JSON.stringify(args)} ${JSON.stringify(env)}`)
spawned = true
@@ -212,19 +206,17 @@ describe('applyCodexLink', () => {
hermesHome: PATHS.hermesHome
})
// bridge.json → start → verified answer → config.toml (backed up) → login item
// bridge.json → start → verified answer → config.toml → login item
expect(world.events).toEqual([
`write ${BRIDGE_JSON}`,
`spawn ${PATHS.pythonw} ${JSON.stringify(args)} ${JSON.stringify(env)}`,
'status ok',
`backup ${BACKUP}`,
`write ${PATHS.codexConfig}`,
'autostart set'
])
expect(JSON.parse(world.files.get(BRIDGE_JSON) ?? '')).toEqual(codexBridgeConfig())
expect(world.files.get(PATHS.codexConfig)).toBe(`${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
expect(world.files.get(BACKUP)).toBe(USER_CONFIG)
expect(world.autostart).toBe(
`"${PATHS.pythonw}" "${PATHS.bridgeScript}" --home "${PATHS.bridgeHome}" --hermes-home "${PATHS.hermesHome}"`
)
@@ -238,7 +230,6 @@ describe('applyCodexLink', () => {
expect(status).toMatchObject({ enabled: true, bridgeRunning: false, lastError: 'bridge-start-failed' })
expect(spawns(world)).toHaveLength(1)
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.files.has(BACKUP)).toBe(false)
expect(world.autostart).toBeNull()
expect(writes(world)).toEqual([`write ${BRIDGE_JSON}`])
// The start poll is bounded by its one budget.
@@ -289,15 +280,6 @@ describe('applyCodexLink', () => {
expect(spawns(world)).toEqual([])
})
it('never overwrites the first backup', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(BACKUP, 'the original original')
await link.apply()
expect(world.files.get(BACKUP)).toBe('the original original')
})
it('falls back to python.exe when pythonw.exe is missing', async () => {
const { link, world } = createWorld()
world.files.delete(PATHS.pythonw)
+1 -10
View File
@@ -23,7 +23,6 @@ import path from 'node:path'
import {
addCodexLinkLine,
CODEX_BRIDGE_VERSION,
CODEX_LINK_BACKUP_SUFFIX,
CODEX_LINK_LAUNCHER_NAME,
CODEX_LINK_REFRESH_URL,
CODEX_LINK_RUN_KEY,
@@ -67,8 +66,6 @@ export interface CodexLinkDeps {
readText: (file: string) => null | string
/** tmp + rename; creates the parent folder. */
writeTextAtomic: (file: string, text: string) => void
/** Copy `source` to `target` unless `target` already exists. */
backupOnce: (source: string, target: string) => void
/** Start the bridge detached and hidden; it must outlive the IDE. */
spawnBridge: (command: string, args: string[], env: Record<string, string>) => void
/** Parsed JSON body, or null when nothing answered within `budgetMs`. */
@@ -82,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.
@@ -334,7 +331,6 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
if (edit.changed) {
try {
deps.backupOnce(paths.codexConfig, `${paths.codexConfig}${CODEX_LINK_BACKUP_SUFFIX}`)
deps.writeTextAtomic(paths.codexConfig, edit.text)
} catch (error) {
fail('config-write-failed', error)
@@ -472,11 +468,6 @@ export function createNodeCodexLinkDeps(paths: CodexLinkPaths, log: (message: st
exists: file => fs.existsSync(file),
readText: readTextLossless,
writeTextAtomic,
backupOnce: (source, target) => {
if (!fs.existsSync(target)) {
fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL)
}
},
spawnBridge: (command, args, env) => {
fs.mkdirSync(paths.bridgeHome, { recursive: true })
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.21",
"version": "1.0.0-beta.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.21",
"version": "1.0.0-beta.27",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",