Compare commits

...
Author SHA1 Message Date
yilsem 1c6e3656b8 ci: verify combined Codex and Jev settings in Windows release
Server CI and Windows package / verify (push) Successful in 2m31s
Server CI and Windows package / windows-package (push) Successful in 14m14s
2026-10-13 01:24:40 +03:00
yilsem 883b1dc824 feat(settings): configure Jev evaluation and model transitions 2026-10-13 01:07:58 +03:00
yilsem 369f6c2ca8 fix(codex-link): isolate model transfers and widen local connection capacity
Server CI and Windows package / verify (push) Successful in 2m3s
Server CI and Windows package / windows-package (push) Successful in 6m58s
2026-10-13 00:35:08 +03:00
yilsem 2718b460ed fix(voice): keep watcher alive through quiet checks and reconnects
Server CI and Windows package / verify (push) Successful in 3m5s
Server CI and Windows package / windows-package (push) Successful in 13m2s
2026-10-12 20:06:46 +03:00
yilsem b4e8664115 Normalize native launcher source and test file endings
Server CI and Windows package / verify (push) Successful in 1m44s
Server CI and Windows package / windows-package (push) Successful in 9m44s
2026-10-12 19:28:37 +03:00
yilsem b66d9164ad Wire authenticated voice watch notices through an owned native launcher 2026-10-12 19:24:33 +03:00
yilsem eeee2756c7 release(desktop): beta29 with Sesli bridge version 9
Server CI and Windows package / verify (push) Successful in 42s
Server CI and Windows package / windows-package (push) Successful in 9m11s
2026-10-12 17:38:12 +03:00
yilsem 017c6629d2 fix(codex-bridge): exempt the voice combo from Jev checks 2026-10-12 16:42:22 +03:00
yilsem 8bd662ea92 fix(desktop): download target Electron for cross-platform packaging
Server CI and Windows package / verify (push) Successful in 41s
Server CI and Windows package / windows-package (push) Successful in 3m25s
2026-10-11 17:47:43 +03:00
yilsem b29cf1ad11 fix(desktop): request Windows libc variant for cross packaging
Server CI and Windows package / windows-package (push) Failing after 1m58s
Server CI and Windows package / verify (push) Successful in 50s
2026-10-11 17:43:38 +03:00
yilsem d0810c63dd fix(desktop): fetch Windows window binding in Linux packaging
Server CI and Windows package / verify (push) Successful in 58s
Server CI and Windows package / windows-package (push) Failing after 1m59s
2026-10-11 17:39:23 +03:00
yilsem e6cd197a2e fix(bridge): load bundled Windows libraries only on Windows 2026-10-11 16:59:08 +03:00
yilsem 02a5fa2080 fix(ci): use bridge test environment with zstandard 2026-10-11 16:57:06 +03:00
yilsem 4f0476e5f2 test(codex-link): use host path separators in bridge startup checks 2026-10-11 16:18:05 +03:00
yilsem 3726d2a553 fix(codex-bridge): preserve tool model and stream preparation without Jev fallback 2026-10-11 15:51:43 +03:00
yilsemandClaude Opus 5.5 8fbbe226db Kopru: son katmanin dondurdugu oldugu gibi gider, sarmalayici yok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:43:00 +03:00
yilsemandClaude Opus 5.5 06039b904d Kopru: secili model hata verirse siradaki katman yapar
Hata kodu, baglanamama ve yarida kopma da siradaki katmana gider.
Siradakine gecilen adimda onceki cevap verilmez; son katman da
acilamazsa onun kendi hatasi gider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:40:13 +03:00
yilsemandClaude Opus 5.5 7547de1852 Kopru: her adim Jev'e gider, sure ve boyut siniri yok
Yazisiz adimda cagrilan komut sorulur; kullanici mesaji bulunamasa da
sorulur; Jev'e giden istek ve cevap kesilmez, bekleme suresi siniri yok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:21:12 +03:00
yilsemandClaude Opus 5.5 ff53cafb5c Kopru: bos donen adim kabul edilmez, siradaki katman yapar
Ne yazi ne arac cagrisi iceren yanit (hata olayi dahil) cevap diye
verilmiyor; secili modelde de katmanda da siradaki katmana geciliyor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:03:40 +03:00
yilsemandClaude Opus 5.5 77504b9aa4 Kopru: hata donduren katman atlanir, Jev devre kesicisi kalkti, ek model siniri 300
Katman-2 Codex isteginde hata olayi donduruyor, kopru bunu cevap diye
gonderiyor ve Codex ayni adimi bastan deneyip turu cevapsiz birakiyordu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:40:44 +03:00
yilsemandClaude Opus 4.8 11bbaddcc7 feat(codex-bridge): Jev checks every added model, routing a block to Katman-1 first
Any added model's answer is now checked, not just the layer combos; a blocked
answer falls to Katman-1, then 2, 3, 4. A selected layer only tries the ones
after it; Katman-4 is delivered unasked. Layer fallbacks always open through
the AITURK gateway, which hosts the combos, whatever route the model used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:15:59 +03:00
yilsemandClaude Opus 5.5 be0b33e89b feat(codex-bridge): Jev routes a blocked step down the layer combos, no exceptions
Katman-1..3 answers are checked by Jev each step; a blocked step is redone by
the next layer combo. Katman-4 is delivered unasked. The technical-obstacle,
question and third-party exemptions and the settings overrides are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:06:21 +03:00
yilsemandClaude Opus 4.8 1f4f2de428 feat(codex-bridge): make the step check strict, keep third-party-attack refusals refused
User, 11 Oct 2026: 'Jev geçmek zor olsun.' Lower thresholds (ret 0.5, sart 0.7)
and only let a refusal pass on a genuinely high technical obstacle, so a fake
technical excuse no longer passes. One carve-out stated to the user first: when
the request is to hack, intrude on or surveil a third party's device or account
or to build malware, a refusal is NOT routed to the uncensored fallback; the
refusal stands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 08:19:44 +03:00
yilsemandClaude Opus 5.5 27964e37b0 fix(codex-bridge): a refusal phrased as a question is still blocked
User, 11 Oct 2026: the model answered "I can't write that; shall I do a reduced
version?" and the step check let it through because it was a question. A reply
that refuses or sets its own conditions is blocked even when it ends with a
question; a clarifying question without a refusal stays below the thresholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:07:50 +03:00
yilsemandClaude Opus 5.5 5556ad11af feat(codex-bridge): per-step check with Jev, blocked steps go to a fallback model
User decision (11 Oct 2026): every step of an added model is checked in the
bridge; Jev only passes or blocks; a blocked step is redone by
Uncensored-Uygulama and the next step goes back to the selected model.

The bridge buffers the step's answer, asks Jev four yes/no questions and
decides in code: blocked when the answer refuses or dodges the request, or
changes it with its own conditions without asking; passed when it reports a
concrete technical obstacle, asks the user a question, or has no text (tool
calls only). While buffering the client gets SSE comment lines. Jev unreachable
or fallback unavailable: the first answer is delivered. Codex's own models do
not reach the bridge and are not checked.

Also logs the shape of each forwarded request (not its content): the recurring
"upstream 400" turned out to be Codex's trailing configuration_update item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:56:27 +03:00
yilsemandClaude Opus 5.5 c8993d58cc feat(codex-bridge): web search for added models, forgiving spoken model names
Added models had no web search: Codex's built-in one runs on OpenAI's side and
the bridge drops it from their tool list. Measured in a voice session on
11 Oct 2026: a model looked for a search tool ten times and ended the turn
with no answer. The bridge now tells added models two ready commands (ara.py:
search and read a page, standard library only, no key). Codex's own models
never see the note: their requests do not reach the bridge.

Spoken model names: a half-spoken word matches the start of a name part
("think" -> thinking, "obli" -> obliterated), and when one candidate is the
others minus extra words, the one without the unsaid suffix is picked
("Qwen 3.8 Obliterated" -> not the ":thinking" one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:25:30 +03:00
yilsemandClaude Opus 5.5 23e8aa8191 feat(codex-bridge): split into a splitter and a separate bridge process
bridge.py is now a small splitter with no features: a request for one of
Codex's own models goes to chatgpt.com byte for byte, a request for an added
model goes to kopru.py, which runs as its own process. Codex's own models no
longer pass through the feature code at all, and restarting or losing the
bridge process does not touch them.

Measured with real Codex on spare ports: own model answers; forced compaction
on the own model completes (twice) while the bridge process is restarted;
added model answers; no own-model request reaches the bridge process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:13:29 +03:00
yilsemandClaude Opus 5.5 c0eee2cc2a fix(codex-bridge): do nothing at all for Codex's own models
User, 11 Oct 2026: 'If you pick Codex and write to Codex, our bridge cannot be
beside it even to help.' The last touch is gone too: traces an added model or
the bridge left earlier in the chat are no longer cleaned on the way to
chatgpt.com. The request goes out byte for byte.

Known consequence: a chat that used an added model, or still carries a
bridge-made summary, may be refused by chatgpt.com on Codex's own model; such a
chat continues on the added models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:27:54 +03:00
yilsemandClaude Opus 5.5 12eeec7b0f fix(codex-bridge): leave Codex's own models alone
User decision, 11 Oct 2026: Codex's own (subscription) models are one side,
the models we add are the other. A request for one of Codex's own models now
goes to chatgpt.com as it is: no model command, lone-dot answer, info note,
work-line instruction, half-answer note or compaction handling. Codex compacts
those chats itself; the bridge making the summary on gpt-6-astra failed four
times this morning and left Codex with 'Error running remote compact task'.

Only the bridge's own traces are still cleaned (foreign ids and bridge-made
summaries left by an added model earlier in the same chat); without any, the
body is byte-identical.

Voice switching stays among the added models. Asking for one of Codex's own
models by voice is answered with how to do it: pick it in the model picker and
send a lone dot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:16:26 +03:00
yilsem dd5f99c406 Move IDE sources and Windows packaging to the new server
Server CI and Windows package / verify (push) Failing after 2m2s
Server CI and Windows package / windows-package (push) Skipped
2026-10-11 04:17:46 +03:00
yilsemandClaude Opus 5.5 48103b9ad3 feat(codex-bridge): the AITURK work line is part of Codex Baglama
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) Canceled after 0s
ci / approval-gate-windows (push) Canceled after 0s
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) Canceled after 0s
ci / approval-gate-windows (push) Canceled after 0s
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 / mobile-link-windows (push) Canceled after 0s
ci / approval-gate-windows (push) Canceled after 0s
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)
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
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
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)
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
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
74 changed files with 8284 additions and 1452 deletions

No files matched your search

+73 -104
View File
@@ -1,118 +1,87 @@
# AITURK IDE ajan CI (kart turkservis-20261007-1421-2).
#
# Bu depo bir yukari akis catalidir; .github/workflows altindaki GitHub is akislari Gitea'da kosmaz. .gitea/workflows
# klasoru var oldugu surece Gitea yalniz buradaki is akislarini okur.
#
# Ne kosar: korumali talimat dosyasi onay kapisinin testleri (insana sorulamayan kosuda aninda BLOCKED, gec onayin
# uygulanmamasi, yolo ile kapinin acilmamasi) ve dosya yazma guvenligi testleri. Kosucu Windows'tur (etiket
# windows-latest; 7 Ekim 2026'da cevrimici kosucu hermes-laptop-windows): urunun hedefi Windows x64'tur ve hata
# Windows asama kosusunda gorulmustur.
#
# Ne sayilmaz (durustluk notu): tests/tools/test_file_write_safety.py icindeki 7 test Windows'ta bu degisiklikten
# bagimsiz olarak gecemez; yukari akis bunlari POSIX icin yazmistir (/etc, /private/etc, /boot yollari; yonetici
# ayricaligi isteyen symlink; POSIX stat alanlari). Asagida adlariyla deselect edilir; degismemis 58dbb4523b
# commit'inde de ayni 7 test kaliyordu (7 Ekim 2026 olcumu). Tam yukari akis test takimi burada KOSMAZ.
name: ci
name: Server CI and Windows package
on:
push:
branches:
- aiturk/main
- fix/ide-windows-vcruntime
branches: [aiturk/main, fix/ide-windows-vcruntime]
workflow_dispatch:
permissions:
contents: read
jobs:
mobile-link-windows:
runs-on: windows-latest
timeout-minutes: 15
steps:
- 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
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$deps = Join-Path $env:RUNNER_TEMP "mobile-tests-$env:GITHUB_RUN_ID"
New-Item -ItemType Directory -Force -Path $deps | Out-Null
npm install --prefix $deps --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
if ($LASTEXITCODE -ne 0) { throw 'Test bagimliliklari kurulamadi' }
New-Item -ItemType Junction -Path node_modules -Target (Join-Path $deps 'node_modules') | Out-Null
node node_modules/vitest/vitest.mjs run --root apps/desktop --config vitest.mobile.config.ts
exit $LASTEXITCODE
- name: Codex koprusu (Python, bagimliliksiz) testleri
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$py = (Get-Command python -ErrorAction SilentlyContinue).Source
if ($py) { & $py -m unittest discover -s apps/desktop/assets/codex-bridge }
else { & py -3 -m unittest discover -s apps/desktop/assets/codex-bridge }
exit $LASTEXITCODE
approval-gate-windows:
runs-on: windows-latest
verify:
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
NODE_OPTIONS: --max-old-space-size=2560
PYTHONDONTWRITEBYTECODE: '1'
steps:
- name: Kaynagi al
uses: https://gitea.twinpay.one/yilsem/ci-actions-checkout@11d5960a326750d5838078e36cf38b85af677262 # upstream v4.4.0
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.twinpay.one
- name: Ortami kur
shell: powershell
github-server-url: https://gitea.turkservis.online
- name: Exact source and Linux test environment
run: |
$ErrorActionPreference = 'Stop'
"kosucu: $env:COMPUTERNAME / $env:USERNAME"
"checkout SHA: $(git rev-parse HEAD) / beklenen: $env:GITHUB_SHA"
$py = $null
$launcher = Get-Command py -ErrorAction SilentlyContinue
if ($launcher) { $py = (& $launcher.Source -3 -c 'import sys; print(sys.executable)').Trim() }
if (-not $py) {
$cmd = Get-Command python -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source -notlike '*\WindowsApps\*') { $py = $cmd.Source }
}
if (-not $py) { throw 'Python bulunamadi' }
"onyukleme python: $py"
& $py -m pip install --quiet --disable-pip-version-check --target "$env:RUNNER_TEMP\uvboot" uv
if ($LASTEXITCODE -ne 0) { throw 'uv kurulamadi' }
$env:PYTHONPATH = "$env:RUNNER_TEMP\uvboot"
& $py -m uv venv --python 3.11 "$env:RUNNER_TEMP\ci-venv"
if ($LASTEXITCODE -ne 0) { throw 'venv olusturulamadi' }
& $py -m uv pip install --python "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe" -e . pytest
if ($LASTEXITCODE -ne 0) { throw 'bagimliliklar kurulamadi' }
$env:PYTHONPATH = ''
& "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe" --version
- name: Onay kapisi testleri
shell: powershell
set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
python3 -m venv "$RUNNER_TEMP/bootstrap"
"$RUNNER_TEMP/bootstrap/bin/pip" install uv
"$RUNNER_TEMP/bootstrap/bin/uv" venv --python 3.11 "$RUNNER_TEMP/ci-venv"
"$RUNNER_TEMP/bootstrap/bin/uv" pip install --python "$RUNNER_TEMP/ci-venv/bin/python" -e . pytest zstandard
- name: Approval gate and file safety on the server
run: |
$ErrorActionPreference = 'Continue'
$env:PYTHONDONTWRITEBYTECODE = '1'
$env:PYTHONUTF8 = '1'
$venv = "$env:RUNNER_TEMP\ci-venv\Scripts\python.exe"
$f = 'tests/tools/test_file_write_safety.py'
$sayilmayan = @(
"${f}::TestCheckSensitivePathMacOSBypass::test_etc_hosts_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_etc_hosts_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_etc_ssh_config_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_private_var_blocked",
"${f}::TestCheckSensitivePathMacOSBypass::test_boot_still_blocked",
"${f}::TestAtomicWrite::test_patch_routes_through_atomic_write",
"${f}::TestProtectedInstructionFiles::test_symlink_to_protected_file_is_gated"
)
$deselect = @()
foreach ($t in $sayilmayan) { $deselect += '--deselect'; $deselect += $t }
"CI OZETI: Windows'ta sayilmayan (deselect) test sayisi: $($sayilmayan.Count)"
$sayilmayan | ForEach-Object { " sayilmadi: $_" }
& $venv -m pytest tests/tools/test_protected_instruction_unattended.py $f `
tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py `
tests/cli/test_single_query_session_finalize.py `
tests/hermes_cli/test_plugin_catalog.py tests/hermes_cli/test_aiturk_plugin_catalog.py `
tests/hermes_cli/test_plugin_install_ref.py `
tests/tui_gateway/test_mobile_subagents.py @deselect -q -p no:cacheprovider -rf
$kod = $LASTEXITCODE
"pytest cikis kodu: $kod"
exit $kod
"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_protected_instruction_unattended.py tests/tools/test_file_write_safety.py tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py tests/cli/test_single_query_session_finalize.py -q -p no:cacheprovider -rf
- name: Codex bridge regression tests
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
- name: Native watch and speech mux pytest regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py -q -p no:cacheprovider -rf'
- name: Mobile and desktop startup regression tests
run: |
npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
ln -s "$RUNNER_TEMP/mobile-tests/node_modules" node_modules
node node_modules/vitest/vitest.mjs run --root apps/desktop --config vitest.mobile.config.ts --maxWorkers=2
windows-package:
if: github.ref == 'refs/heads/fix/ide-windows-vcruntime'
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 60
# Windows packaging runs on Linux/Wine in the new server. No laptop runner.
container: electronuserland/builder@sha256:b226b27b1a7e95ad25eb3d6414ea90c56c04eb6d8329967f4d721dce50b989ff
env:
NODE_OPTIONS: --max-old-space-size=2560
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
steps:
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.turkservis.online
- name: Install locked source dependencies
run: npm ci --no-audit --no-fund
- name: Install Windows launcher cross compiler and framework reference
run: |
set -eu
apt-get update
apt-get install -y --no-install-recommends mono-mcs=6.8.0.105+dfsg-3.2 libmono-system-web-extensions4.0-cil=6.8.0.105+dfsg-3.2
- name: Compile Windows launcher from source and verify failure gate
run: node --test apps/desktop/scripts/build-native-watch-launcher.test.mjs
- name: Native dependency staging regression tests
run: npx vitest run --root apps/desktop scripts/stage-native-deps.test.mjs --maxWorkers=2
- name: Codex connection settings regression tests
run: npx vitest run --root apps/desktop electron/codex-link.test.ts electron/codex-link-config.test.ts electron/staged-transition.test.ts src/app/settings/codex-link-settings.test.tsx src/app/settings/staged-transition-settings.test.tsx --maxWorkers=2
- name: Build and package Windows x64
run: |
set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
npm run build --workspace=apps/desktop
npm run builder --workspace=apps/desktop -- --win nsis zip --x64 --publish never
git diff --exit-code
cd apps/desktop/release
sha256sum *.exe *.zip > SHA256SUMS
- name: Preserve exact-source Windows package
uses: actions/upload-artifact@v3
with:
name: aiturk-ide-windows-${{ github.sha }}
if-no-files-found: error
path: |
apps/desktop/release/*.exe
apps/desktop/release/*.zip
apps/desktop/release/SHA256SUMS
+1 -1
View File
@@ -54,5 +54,5 @@ Nous Research telif bildirimi ve MIT metni [LICENSE](LICENSE) dosyasında korunu
paket ayrıca `assets/NOTICE.txt` ve `assets/LICENSE.txt` içerir.
AITURK/TurkServis dağıtımı Nous Research tarafından onaylanmış bir ürün değildir.
[AITURK kaynak deposu](https://gitea.twinpay.one/yilsem/aiturk-hermes-ide)
[AITURK kaynak deposu](https://gitea.turkservis.online/yilsem/aiturk-hermes-ide)
· [TurkServis](https://turkservis.online)
@@ -0,0 +1,2 @@
# Generated by beforePack from NativeWatchBootstrap.cs; never commit opaque output.
/native-watch.exe
@@ -0,0 +1,136 @@
# Native watch launcher — reviewable Windows candidate
This is an owned startup hook for the original Codex engine, not another model
engine. Do not activate it until root has built a new packaged release and
reviewed the evidence. No environment variable is set by build/provision tools.
## Build and provision
Normal Windows `beforePack` invokes `scripts/build-native-watch-launcher.mjs`.
The resulting `native-watch.exe` is included in the existing codex-bridge
extraResources directory along with Python modules. Windows hosts use the .NET Framework C# compiler. The existing Linux/Wine
Windows-package job installs mono-mcs and libmono-system-web-extensions4.0-cil
6.8.0.105+dfsg-3.2 from its Ubuntu 22.04 apt repository and compiles with mcs.
Both hosts rebuild the same C# source; missing compilers or invalid source fail
packaging and remove stale output. The generated EXE is ignored by git.
This is a Windows artifact cross-build, not a Linux runtime launcher. The executable is a managed console bootstrap; use
an absolute managed **python.exe**, not pythonw.exe. It checks pinned hashes of
original binary, interpreter and launcher script. Original is checked again in
Python immediately before use. Parent directory ACLs and the selected official
binary hash are provisioning authority; this does not claim Authenticode trust.
After installing the new package, root provisions an owner-local candidate:
```powershell
py -3 prepare_native_watch.py --source <packaged-codex-bridge> --destination <owned-candidate-directory> --original <official-absolute-codex.exe> --expected-sha256 <independently-verified-official-sha256> --python <managed-python.exe> --bridge-home <bridge-data-directory> --hermes-home <existing-agent-home>
```
`prepare_native_watch.py` copies only the relevant resources and writes the
adjacent native-watch.json (paths/hashes only, no keys). It never reads
CODEX_CLI_PATH to locate the original and rejects its own executable as original.
Root alone may later point Desktop's CODEX_CLI_PATH at this EXE. No Python argv
are placed inside CODEX_CLI_PATH. No installed app.asar/binary modification.
Rollback is root removing its override to restore original app startup.
Installed app read-only inspection (OpenAI.Codex_26.1007.2314.0):
`.vite/build/application-network-startup-CfAQmSYg.js` tc()/Ks() constructs
`-c` pairs before/after `app-server`, `--analytics-default-enabled`. Recognizer
accepts that grammar plus explicit stdio forms, rejects daemon/websocket/unknown
commands and passes those unchanged to the original. Approvals flow untouched.
One original engine only; no extra version process at startup. Original cwd and
args preserved except one per-child `-c openai_base_url` selecting the in-process
bridge's ephemeral loopback `/backend-api/codex`. Normal original models and
public member catalog reuse kopru.H. Native environment is restored after bridge
import. Bridge logging uses stderr so stdout remains native JSONL only.
## Receipt, delivery and lifecycle
Before routing/rewrite, all three actual request values must agree:
session-id, thread-id and prompt_cache_key. The active mux connection ID and
realtime generation are captured then. Neither current tab nor notice data can
create that mapping. thread/unsubscribe, realtime stop/start/closed/error and EOF
invalidate an epoch. UI-only switches not observable in native protocol cannot
be detected here; root must verify Desktop emits stop/unsubscribe for its switch.
Concurrent active native threads have independent request mappings.
SSE `event: bekci.watch` and nonstream `_bekci_watch` are removed before native
model text, history observers, response rewriting and response preview logging.
Only `/v1/bekci/events` and the fixed `https://ai.turkservis.online` origin are
accepted. The same primary route public member key authenticates POST; redirects
are not followed. No backend keys, raw server tenant/session/history identifiers,
or reconstructed history exist in this client.
Only authenticated SSE `ready` binds the receipt. Binding's legacy tenant_id,
bekci_session_id, history_revision fields contain **local opaque scopes**, not
literal server claims: member-key fingerprint, key+public-subscription digest,
public-receipt revision digest. Server remains authoritative for hidden history.
TLS plus same-key proxy ready is the receipt validity attestation. SQLite also
locks receipt scope to its original connection/thread/generation across restarts.
SQLite WAL/FULL ledger commits `uncertain` before enqueue. Replays or a crash
between commit and append are suppressed, including possible lost delivery.
Cursor is persisted after durable admission/disposition; it is a local replay
ack, not a server consumption API or exactly-once guarantee. Native RPC acceptance
does not prove audio output. Replay IDs never cause automatic uncertain resend.
Keepalives, bounded reconnect attempts, receipt expiry, lifecycle abort and
simultaneous threads are handled. Receipt expiry invalidates its local binding.
No raw notice text or credentials are persisted in the ledger.
## Evidence and exact remaining operations
- `.claude/sesli-client-all-tests.log`: 20 files, 203 passed, 0 failed.
- `.claude/sesli-client-tests-red.log`: compiled Windows bootstrap lost stdout/
stderr before fix; interactive flush subsequently exposed by real native probe
and covered by `test_executable_flushes_interactive_stdin_before_eof`.
- `.claude/native-launcher-proof.json`: compiled candidate, pinned official native
initialize accepted, stdout JSON, EOF exit 0. No native voice call performed.
- `.claude/native-launcher-candidate/native-watch.exe`: concrete provisioned
candidate under worktree, not activated or installed.
Source now stamps desktop beta.30 and bridge 10. Root still must run
normal full package CI/build/signing policy, inspect packaged resources, provision the
new packaged candidate with trusted official SHA/managed runtime, activate only
authorized Desktop startup, verify thread-switch notifications and exercise a
real Sesli-Hizli-Zeki call through deployed server to audible appendSpeech. The bootstrap assigns itself to a non-inheritable KILL_ON_JOB_CLOSE Windows
Job Object before Python starts. Its descendants inherit membership; abrupt
bootstrap termination closes the handle and kills only that owned tree. Owned
fake and pinned original initialize/kill tests passed for both Windows-built and
Linux-built EXEs running on Windows. Actual Desktop UI termination remains a
manual acceptance step. Atomic generation enforcement inside native appendSpeech
is still unavailable (the native RPC has no expected-generation field).
No commit, push, deployment, installed runtime change or AITURK delegation.
## 2026-10-12 source verification update
- `.claude/native-watch-linux-build.log`: exact CI image digest
`sha256:b226b27b1a7e95ad25eb3d6414ea90c56c04eb6d8329967f4d721dce50b989ff`,
real apt dependencies, two compiler regressions passed, Windows PE produced.
- `.claude/native-watch-windows-build.log`: same two tests passed with Framework csc.
- `.claude/native-watch-linux-artifact-windows-tests.log`: 25 tests passed with
the cross-built artifact, including pinned original initialize and abrupt kill.
- `.claude/sesli-client-release-all-tests.log`: 220 passed, 21 files, zero failures.
- `.claude/electron-beta30-tests.log`: 47 passed including Python/Electron version alignment.
Receipt validation accepts up to 24 hours plus 60 seconds clock skew. Actual
expiry remains authoritative; expired, boolean, nonnumeric, NaN, infinite and
over-bound receipts are rejected. Long research has no local 15-minute limit.
Provision the exact rebuilt candidate with the explicit original SHA-256
`d13914ced6c7af174d8d638db938284231227312104b06a25e9608c632a7daec` only
when that is the selected independently verified official original. SHA-1 is
not the executable integrity pin; a Git source identity and an executable
SHA-256 have different roles. Fixed-origin TLS plus the same public member key
and authenticated ready event attest the receipt; none claims Authenticode.
No full installer build, signing, activation, or audible acceptance is claimed.
CI verify explicitly runs test_bekci_watch.py and test_native_speech_mux.py using
ci-venv pytest in addition to the unchanged unittest suite. Both focused files
pass on Linux without skips. Tests that execute native-watch.exe reside in the
explicitly Windows-only test_native_watch_cleanup.py; their owned-process proof
is Windows evidence, not Linux CI runtime coverage.
Sideband JSON extraction and SSE filtering apply only to the resolved exact
Sesli-Hizli-Zeki target, including when no watch owner is active. Other models
retain their original Jev/response pipeline without the sideband frame buffer.
After the final scope correction, rebuild/provision from final source; the older
local candidate hash proof does not include this last kopru.py change.
@@ -0,0 +1,89 @@
// Thin Windows CLI_PATH executable. Runtime and original are owner-provisioned,
// absolute, SHA-pinned; never consult PATH or CODEX_CLI_PATH.
using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Web.Script.Serialization;
class NativeWatchBootstrap {
// Assign this bootstrap BEFORE creating Python. Descendants inherit job
// membership atomically; the non-inheritable handle belongs only to us.
// Windows closes it even on TerminateProcess, killing Python and native.
static IntPtr ownedJob;
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
public long ProcessTime, JobTime;
public uint Flags;
public UIntPtr MinWorkingSet, MaxWorkingSet;
public uint ActiveProcesses;
public UIntPtr Affinity;
public uint Priority, Scheduling;
}
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
}
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
public BasicLimits Basic;
public IoCounters Io;
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
}
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
static extern IntPtr CreateJobObject(IntPtr attributes, string name);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
static void OwnProcessTree() {
ownedJob=CreateJobObject(IntPtr.Zero,null);
var limits=new ExtendedLimits(); limits.Basic.Flags=0x2000; // KILL_ON_JOB_CLOSE
if(ownedJob==IntPtr.Zero || !SetInformationJobObject(ownedJob,9,ref limits,(uint)Marshal.SizeOf(typeof(ExtendedLimits)))
|| !AssignProcessToJobObject(ownedJob,Process.GetCurrentProcess().Handle)) throw new Exception();
// Do not close here or in finally: the bootstrap is itself a member.
// The OS closes this handle at exit, preserving our native exit code.
}
static string Quote(string s) {
var b=new StringBuilder("\""); int slashes=0;
foreach(char c in s) {
if(c=='\\') {slashes++; continue;}
if(c=='"') { b.Append('\\',slashes*2+1); b.Append(c); }
else {b.Append('\\',slashes); b.Append(c);}
slashes=0;
}
b.Append('\\',slashes*2); b.Append('"'); return b.ToString();
}
static string Pin(Dictionary<string,object> c,string field) {
string p=(string)c[field], expected=(string)c[field+"_sha256"];
if(!Path.IsPathRooted(p) || String.Equals(Path.GetFullPath(p),Process.GetCurrentProcess().MainModule.FileName,StringComparison.OrdinalIgnoreCase)) throw new Exception();
using(var f=File.OpenRead(p)) using(var sha=SHA256.Create()) {
string actual=BitConverter.ToString(sha.ComputeHash(f)).Replace("-","").ToLowerInvariant();
if(actual!=expected.ToLowerInvariant()) throw new Exception();
}
return p;
}
static void Pump(Stream source, Stream target) {
byte[] buffer=new byte[8192]; int count;
while((count=source.Read(buffer,0,buffer.Length))>0) { target.Write(buffer,0,count); target.Flush(); }
}
static int Main(string[] args) {
try {
string manifest=Path.ChangeExtension(Process.GetCurrentProcess().MainModule.FileName,".json");
var c=new JavaScriptSerializer().Deserialize<Dictionary<string,object>>(File.ReadAllText(manifest));
string original=Pin(c,"original");
string python=Pin(c,"python");
string script=Pin(c,"script");
OwnProcessTree();
var argv=new List<string>{"-u",script,manifest}; argv.AddRange(args);
var quoted=new List<string>(); foreach(var a in argv) quoted.Add(Quote(a));
var p=Process.Start(new ProcessStartInfo(python,String.Join(" ",quoted.ToArray())) { UseShellExecute=false, CreateNoWindow=true, RedirectStandardInput=true, RedirectStandardOutput=true, RedirectStandardError=true });
var input=new Thread(()=> { try { Pump(Console.OpenStandardInput(),p.StandardInput.BaseStream); p.StandardInput.Close(); } catch(IOException) {} });
input.IsBackground=true; input.Start();
var output=new Thread(()=>Pump(p.StandardOutput.BaseStream,Console.OpenStandardOutput()));
var error=new Thread(()=>Pump(p.StandardError.BaseStream,Console.OpenStandardError()));
output.Start(); error.Start();
p.WaitForExit(); output.Join(); error.Join(); return p.ExitCode;
} catch { Console.Error.WriteLine("Owned native launcher integrity/configuration failure."); return 78; }
}
}
+57 -5
View File
@@ -13,10 +13,10 @@ Codex depends on it while `openai_base_url` points here.
The public `/models` catalog supplies every OmniRoute chat model; a member does
not need a private local model allowlist. `/router/catalog` supplies only the
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
entries and internal 9Router combos are excluded. Combos sort A–Z first, then
individual models by provider and name, including OpenAI subscription models.
Codex labels the first visible row `isDefault`; a saved model selection remains
unchanged. A failed or empty refresh keeps the last successful catalog.
entries and internal 9Router combos are excluded. Codex's own subscription models
stay first, in their own order and under their own names; added entries follow
them: combos A–Z, then individual models by provider and name. A saved model
selection remains unchanged. A failed or empty refresh keeps the last successful catalog.
- `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made
by one provider stays readable after the user switches to another.
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
@@ -27,6 +27,35 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory.
## Version 11: isolated routes and cross-provider preparation
- Pure subscription chats and native voice retain their ChatGPT route. Added
models use the separate AITURK worker; model selection never globally changes
the provider for another chat.
- Cross-provider history repairs custom tool IDs and unwraps old AITURK summaries
before sending them to OpenAI. Real OpenAI encrypted compaction is interpreted
by OpenAI before transfer; unreadable context stops the transfer instead of
silently dropping it. The saved transcript is not rewritten.
- Old context above 70% of the target window is summarized through AITURK, with
instructions, tools, recent messages and complete tool pairs protected. Output
allowance is reserved. Unknown windows use a conservative guard. Summaries are
approximate; if protected content alone cannot fit, a visible error keeps the
source history intact. No middle-of-message truncation is used by this path.
- Preparation emits SSE keep-alive comments while it waits. The status version
matches the IDE, avoiding needless healthy-process restarts on reconnect.
- Both HTTP listeners accept a burst queue of 128 instead of Python 3.11's five;
disk logging and transfer records no longer share the request-counter lock.
This is local capacity, not a change to any provider's rate or account limits.
- The catalog cap is 1000; native entries retain their order, followed by combos
A–Z and provider/model A–Z. The official desktop picker owns submenus: this
bridge does not patch the Codex app or promise provider folders.
- Turning Codex Link off restores its owned config line. If that write fails,
the working bridge remains alive and the UI does not claim restoration.
Acceptance covers socket routing (including native voice endpoints), 50 concurrent
synthetic requests, tool-ID regression, context budgets and summary failures.
Microphone/audio and public installer acceptance are separate release checks.
## 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
@@ -45,6 +74,29 @@ for the chat, i.e. until the user picks another one in the list.
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).
@@ -65,4 +117,4 @@ Not changed: subscription models, their order and names, and everything on the v
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> hazır." and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> seçildi." (the target is not probed, so it does not say "ready") and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
@@ -0,0 +1,44 @@
# Codex sesli entegrasyon incelemesi — 12 Ekim 2026
## Uygulanan değişiklik ve doğrulama
Çözülmüş hedef tam `Sesli-Hizli-Zeki` ise nihai metin Jev'e gönderilmez. Büyük/küçük harf, boşluk, `ide/` öneki veya sonek normalizasyonu yapılmaz. `ide/` kullanıcı slug'ı önce mevcut mapping ile hedefe çözülür. Taşıma hatası, boş yanıt ve kopan akışın katman kurtarması korunur; yedek katmana geçince muafiyet taşınmaz. Araç/commentary hazırlık bypass'ı korunur. İstisna yanıt tamponlamasını değiştirmez.
`test_jev_denetim.py` exact hedefi, benzer isimleri ve fallback katmanının tekrar denetlenmesini çalıştırır. `test_denetim_ek.py` gerçek `_responses` metodu üzerinden başarılı yanıt, bağlantı kopması ve HTTP 400 yollarında çözülmüş hedef aktarımını, `instructions` ve developer mesajının korunmasını doğrular. Bunlar mevcut handler test düzeneğini kullanır; canlı sağlayıcı testi değildir.
Paket README'sindeki komutla `py -3 -m unittest discover`: **185 test, 25.305 saniye, OK**. Mevcut testlerde ResourceWarning ve beklenen yerel bağlantı reddi çıktısı vardı. İlk yeni test eski imzada hata verdi. Ayrıca muafiyet fonksiyonu geçici mock ile False yapıldığında yeni test gerçek davranış farkıyla başarısız oldu: özgün cevap yerine Katman-1 cevabı geldi (`MUTATION_DETECTED=True`).
## Gerçek Codex ses yolu
İncelenen istemci: WindowsApps `OpenAI.Codex_26.1007.2314.0_x64__2p2nqsd0c76g0`, `app/resources/app.asar`, salt okunur. CLI: `codex-cli 0.155.1`. `codex app-server generate-ts --experimental --out <geçici-dizin>` ile yerel binary protokolü üretildi.
Protokolün `ClientRequest.ts` dosyasında gerçek yöntemler:
- `thread/realtime/appendText`: `{threadId, text, role}`; role = user/developer/assistant. Girdi ekler; tek başına ses üretimini garanti ettiği iddia edilmez.
- `thread/realtime/appendSpeech`: `{threadId, text}`; açıklaması “append speakable text to thread realtime”.
- `thread/realtime/start`: thread kapsamlı oturum; websocket, webrtc veya existingCall taşıması. `clientManagedHandoffs` varsayılan false; automatic Codex response handoff seçenekleri var. Başka oturumun existingCall bağlantısı kullanılmadı.
- `turn/start`, `turn/steer`, `thread/queue/add`, `thread/queue/start` var. `turn/steer` aktif `expectedTurnId` ister; bitmiş tura uygulanacak genel uyandırma yolu değildir. `codex queue --thread ... --message ...` mevcut CLI yüzeyidir; kuyruğa koymanın ses çıkışıyla aynı şey olduğu doğrulanmadı.
İstemcinin `webview/assets/app-initial-b8e1c8014bb4.js` dosyasında `cra` ve `lra`, sırasıyla appendText/appendSpeech RPC'sini `manager.sendRequest` ile çağırıyor. Realtime runtime aynı yöntemleri dışarı sunuyor; durdurulmuş durumda no-op. **Somut arka plan tüketicisi:** kullanım limiti yaklaşma bildirimi geldiğinde istemci `appendSpeech` çağırıp uyarıyı sesli söylüyor (`[Composer] failed to speak realtime usage warning` hata kolu). Bu, kullanıcı yeni mesaj yazmadan konuşma istemenin mevcut istemci mekanizması olduğunu kanıtlar. Bekçi düzeltmesinin canlı ses çıkışı test edilmiş değildir.
`CompilerIntrinsics-87ea46a69911.js` içinde WebRTC data channel üzerinden `conversation.item.create` gönderimi de var. Aynı aramada `response.created` olay işleyicisi görüldü; bunu `response.create` gönderim kanıtı saymıyoruz. Ham Realtime protokolü icat etmek yerine mevcut thread realtime RPC'si entegrasyon adayıdır.
## Köprüdeki somut eksik
`bridge.py:H._isle` yalnız `/responses` ve model listesini alt köprüye yollar; diğer yollar, `/realtime/calls` dahil, doğrudan ChatGPT'ye gider. WebSocket upgrade 426 ile reddedilir. Dolayısıyla `kopru.py:H._passthrough` içindeki isteğe bağlı `voice_prep.baslat(Location, headers, ...)` kancası standart ayırıcı topolojisinde çağrı oluşturmayı göremez.
`voice_prep.py` ve `ses_tuneli.py` kaynak worktree ve kurulu AITURK resources/codex-bridge içinde yok. Yerel aramada `Temp/oldb/voice_prep.py` yedek kopyası bulundu ve yalnız okundu: `Hazirlayici` aynı call'a `wss://api.openai.com/v1/live/` sideband açıyor; `session.context.append` ile bağlam ekliyor. Bu eski dosyada response.create/conversation.item.create gönderimi bulunmadı. Yedek dosya kurulu/aktif bileşen kanıtı değildir; çalıştırılmadı veya kopyalanmadı. `ses_tuneli` bulunamadı.
## Bekçi entegrasyonunda kalanlar
Diğer ajanın auth + session + cursor notices SSE sözleşmesi henüz bu çalışmaya ulaşmadı. Sözleşme sonrası bildirimin doğrulanmış session kimliğini doğru Codex host/thread ve canlı realtime ömrüne bağlamak gerekiyor. SSE yeniden bağlanma/cursor tekrarlarında aynı düzeltmeyi iki kez seslendirmeme, bitmiş ses oturumuna göndermeme ve yeni kullanıcı konuşmasıyla çakışmayı ele alma davranışları kabul testine dahil edilmeli. Bunlar uygulanmış sözleşme değildir.
Yalnız bize ait deney oturumunda, ilk ses cevabı bittikten sonra bir notice üretilip mevcut app-server yetkili bağlantısından `thread/realtime/appendSpeech` ile düzeltme konuşmasının başlaması; başka thread'in etkilenmemesi; yeni kullanıcı mesajı gerekmemesi gözlenmeli. Bu canlı test yapılmadan “entegrasyon tamamlandı” denemez. Hermes masaüstü TTS/voice hook'ları bu Codex kanalının yerine kullanılmadı. İmzalı istemciye API eklenmedi.
## Kokla / Codex alt ajan yolu
Mevcut yerel `C:/Users/maste/.codex/skills/codex-kokla-ajan/SKILL.md` açıkça `ide/GPT-6-Astra` CLI slug'ını, `GPT-6-Astra` OmniRoute combo kimliğini belirtiyor. `gpt-6-astra` tek modelini combo yerine kullanmayı yasaklıyor. Burada katalog yeniden sorgulanmadı; bilgi skill'in kaydıdır.
Mevcut CLI yolu: `codex exec --model 'ide/GPT-6-Astra' --color never -C '<ayrı-worktree>' -o '<sonuç>' '<görev>'`. `--json` süreç olaylarını, `exec resume` düzeltme devamını destekliyor. Skill ana ajana süreç kimliği, hedef, kalanlar, sonuç dosyası kaydı; çıktı takibi; bağımsız diff/test kontrolü; sıkıştırma sonrası kayıt ve kanıt okuma sorumluluğu veriyor. Bu görevde yeni bir CLI alt ajanı başlatılmadı. Runtime'ın sunulan `spawn_agent` aracı çağrısı `unsupported call: spawn_agent` döndü; bu nedenle kullanılabilir canlı alt ajan oluşturduğu iddia edilmez.
`C:/Users/maste/.codex/hooks/codex_continuity.py` save işlemi session/cwd kapsamlı; PreCompact yedekliyor, SessionStart/UserPromptSubmit geri yüklüyor, Stop aktif kayıtta devam uyarısı veriyor. Tamamlanma için kalan iş olmaması ve kanıt dosyası şartı var. Bu kayıt bağımsız test yerine geçmez. Runtime plugin CLI add/list/remove sağlar; istemcinin özel JS fonksiyonlarına genel plugin erişimi kanıtlanmadı. AITURK IDE devri yapılmadı.
+121
View File
@@ -0,0 +1,121 @@
"""Internet aramasi ve sayfa okuma: Codex'e sonradan eklenen modeller icin (11 Ekim 2026).
Codex'in yerlesik web aramasi OpenAI sunucusunda calisir ve yalniz Codex'in kendi modellerine verilir; kopru onu
ek modele giden arac listesinden atar. Ek model aramayi bu betigi kabuk araciyla calistirarak yapar (internet.py
modele komutu soyler). Anahtar gerekmez; yalniz standart kitaplik.
python ara.py ara "sorgu" [--adet 8]
python ara.py oku <adres> [--en-cok 8000]
"""
import argparse
import html
import re
import sys
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
ARAMA = "https://html.duckduckgo.com/html/?"
SURE = 25
EN_COK_BAYT = 3_000_000
def indir(adres, sure=SURE):
adres = urllib.parse.quote(adres, safe=":/?#[]@!$&'()*+,;=%~") # Turkce harfli adres ("/wiki/Türkiye")
istek = urllib.request.Request(adres, headers={"User-Agent": UA, "Accept-Language": "tr,en;q=0.8"})
with urllib.request.urlopen(istek, timeout=sure) as yanit:
ham = yanit.read(EN_COK_BAYT)
kodlama = yanit.headers.get_content_charset() or "utf-8"
tur = yanit.headers.get_content_type()
try:
return ham.decode(kodlama, "replace"), tur
except LookupError:
return ham.decode("utf-8", "replace"), tur
def _duz(parca):
return re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", "", parca))).strip()
def sonuclari_coz(sayfa, adet=8):
"""Arama sayfasindan [(baslik, adres, ozet)]."""
cikti = []
for blok in re.split(r'<div[^>]+class="[^"]*\bresult\b[^"]*"', sayfa)[1:]:
bag = re.search(r'<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>', blok, re.S)
if not bag:
continue
adres = html.unescape(bag.group(1))
ic = re.search(r"[?&]uddg=([^&]+)", adres)
if ic:
adres = urllib.parse.unquote(ic.group(1))
if adres.startswith("//"):
adres = "https:" + adres
if "duckduckgo.com/y.js" in adres: # reklam
continue
ozet = re.search(r'class="result__snippet"[^>]*>(.*?)</a>', blok, re.S)
cikti.append((_duz(bag.group(2)), adres, _duz(ozet.group(1)) if ozet else ""))
if len(cikti) >= adet:
break
return cikti
def metne_cevir(sayfa):
"""HTML -> okunur duz metin (betik, bicim ve gezinme kisimlari atilir)."""
sayfa = re.sub(r"(?is)<(script|style|noscript|svg|nav|footer|header|form|iframe)\b.*?</\1>", " ", sayfa)
sayfa = re.sub(r"(?is)<!--.*?-->", " ", sayfa)
sayfa = re.sub(r"(?i)<(br|/p|/div|/li|/h[1-6]|/tr)\b[^>]*>", "\n", sayfa)
metin = html.unescape(re.sub(r"<[^>]+>", " ", sayfa))
satirlar = [re.sub(r"[ \t\r\f\v]+", " ", s).strip() for s in metin.split("\n")]
return "\n".join(s for s in satirlar if s)
def ara(sorgu, adet=8, indirici=indir):
sayfa, _ = indirici(ARAMA + urllib.parse.urlencode({"q": sorgu}))
sonuc = sonuclari_coz(sayfa, adet)
if not sonuc:
if 'no-results' in sayfa.lower() or 'no results found' in sayfa.lower():
return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin."
# DDG bot kontrolünü/bozuk HTML'yi sonuç yok diye göstermeyiz. Tek yedek istek.
try:
rss, _ = indirici('https://www.bing.com/search?' + urllib.parse.urlencode({'format': 'rss', 'q': sorgu}))
kok = ET.fromstring(rss)
if kok.tag != 'rss' or kok.find('channel') is None:
raise ValueError('RSS arama yanıtı değil')
sonuc = [(i.findtext('title') or '', i.findtext('link') or '', _duz(i.findtext('description') or ''))
for i in kok.findall('./channel/item')
if (i.findtext('link') or '').startswith(('https://', 'http://'))][:adet]
except (ValueError, ET.ParseError, OSError) as hata:
raise RuntimeError('Arama hizmeti engellendi veya yanıtı okunamadı; sorguları değiştirerek tekrar etme.') from hata
if not sonuc:
return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin."
return "\n\n".join(f"{i}. {b}\n {a}\n {o}" for i, (b, a, o) in enumerate(sonuc, 1))
def oku(adres, en_cok=8000, indirici=indir):
if not re.match(r"https?://", adres):
adres = "https://" + adres
sayfa, tur = indirici(adres)
metin = metne_cevir(sayfa) if "html" in tur or "<html" in sayfa[:2000].lower() else sayfa
if len(metin) > en_cok:
metin = metin[:en_cok] + f"\n[... kesildi: {len(metin)} karakterin ilk {en_cok} karakteri]"
return metin or "Sayfada okunur metin bulunamadı."
def main(argv=None):
p = argparse.ArgumentParser(description="Internet aramasi ve sayfa okuma")
alt = p.add_subparsers(dest="is_", required=True)
a = alt.add_parser("ara"); a.add_argument("sorgu", nargs="+"); a.add_argument("--adet", type=int, default=8)
o = alt.add_parser("oku"); o.add_argument("adres"); o.add_argument("--en-cok", type=int, default=8000)
s = p.parse_args(argv)
try:
metin = ara(" ".join(s.sorgu), max(1, min(s.adet, 20))) if s.is_ == "ara" else oku(s.adres, max(500, s.en_cok))
kod = 0
except Exception as e:
metin, kod = f"Hata: {type(e).__name__}: {e}", 1
sys.stdout.buffer.write((metin + "\n").encode("utf-8", "replace"))
return kod
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,210 @@
"""Public Omni receipts. All scopes here are LOCAL opaque identities, never backend IDs."""
import hashlib
import http.client
import json
import socket
import sqlite3
import ssl
import threading
import time
from urllib.parse import urlsplit
from native_speech_mux import Binding, WatchReceiptAdapter
EVENTS_PATH = '/v1/bekci/events'
TRUSTED_ORIGIN = 'https://ai.turkservis.online'
# Public protocol lifetime, with bounded server/client clock skew. The server's
# expiry remains the watch deadline; there is no shorter research timeout.
MAX_RECEIPT_TTL = 24 * 60 * 60
RECEIPT_CLOCK_SKEW = 60
def opaque(*parts):
return hashlib.sha256('\0'.join(parts).encode()).hexdigest()
class Frames:
def __init__(self):
self.buf = b''
def feed(self, chunk):
self.buf += chunk
if len(self.buf) > 262144:
raise ValueError('SSE frame too large')
while True:
# Normalize only complete CRLF, including a CR split across reads.
self.buf = self.buf.replace(b'\r\n', b'\n')
if b'\n\n' not in self.buf:
return
frame, self.buf = self.buf.split(b'\n\n', 1)
event, data, cursor = '', [], None
for line in frame.decode('utf-8').split('\n'):
field, _, value = line.partition(':')
value = value.removeprefix(' ')
if field == 'event': event = value
elif field == 'data': data.append(value)
elif field == 'id': cursor = value
yield event, '\n'.join(data), cursor, frame + b'\n\n'
class MetadataFilter:
"""Strip sideband before rewrites, logging or transcript observers."""
def __init__(self, accept):
self.frames, self.accept = Frames(), accept
def feed(self, chunk):
out = []
for event, data, _, raw in self.frames.feed(chunk):
if event == 'bekci.watch':
try: self.accept(json.loads(data))
except (ValueError, TypeError): pass
else: out.append(raw)
return b''.join(out)
def flush(self):
# An incomplete sideband frame must never escape into model text.
raw, self.frames.buf = self.frames.buf, b''
return b'' if b'bekci.watch' in raw else raw
class Ledger:
"""Commit uncertain BEFORE enqueue. Crash ambiguity sacrifices delivery, never resends."""
def __init__(self, path):
self.db = sqlite3.connect(path, check_same_thread=False)
self.lock = threading.Lock()
self.db.execute('PRAGMA journal_mode=WAL')
self.db.execute('PRAGMA synchronous=FULL')
self.db.executescript('CREATE TABLE IF NOT EXISTS delivery(scope TEXT,id TEXT,status TEXT,PRIMARY KEY(scope,id)); CREATE TABLE IF NOT EXISTS cursors(scope TEXT PRIMARY KEY,cursor TEXT); CREATE TABLE IF NOT EXISTS bindings(scope TEXT PRIMARY KEY,epoch TEXT);')
def bind_epoch(self, scope, correlation):
epoch = opaque(correlation.connection_id,correlation.thread_id,str(correlation.generation))
with self.lock, self.db:
self.db.execute('INSERT OR IGNORE INTO bindings VALUES(?,?)',(scope,epoch))
return self.db.execute('SELECT epoch FROM bindings WHERE scope=?',(scope,)).fetchone()[0] == epoch
def claim(self, scope, nid):
with self.lock, self.db:
return self.db.execute('INSERT OR IGNORE INTO delivery VALUES(?,?,?)', (scope,nid,'uncertain')).rowcount == 1
def ack(self, scope, nid, cursor, status):
with self.lock, self.db:
self.db.execute('UPDATE delivery SET status=? WHERE scope=? AND id=?', (status,scope,nid))
self.db.execute('INSERT OR REPLACE INTO cursors VALUES(?,?)', (scope,cursor))
def cursor(self, scope):
with self.lock:
row = self.db.execute('SELECT cursor FROM cursors WHERE scope=?',(scope,)).fetchone()
return row[0] if row else None
class WatchOwner:
def __init__(self, mux, ledger, *, origin=TRUSTED_ORIGIN, test_loopback=False):
u = urlsplit(origin)
if origin != TRUSTED_ORIGIN and not (test_loopback and u.scheme == 'http' and u.hostname == '127.0.0.1'):
raise ValueError('untrusted Omni origin')
self.mux, self.ledger, self.origin = mux, ledger, u
self.adapter = WatchReceiptAdapter(mux)
self.lock, self.watches = threading.Lock(), {}
def capture(self, headers, body):
return self.adapter.capture(headers, body)
def live(self, c):
s = self.mux.snapshot(); t = s['threads'].get(c.thread_id, {})
return (not s['closed'] and not s['injection_disabled'] and s['connection_id'] == c.connection_id
and t.get('active') and t.get('generation') == c.generation)
def accept(self, payload, correlation, key, route_url):
if not correlation or not self.live(correlation): return 'stale_generation'
route = urlsplit(route_url)
if (route.scheme,route.netloc) != (self.origin.scheme,self.origin.netloc) or not key: return 'untrusted_route'
if (not isinstance(payload,dict) or set(payload) != {'subscription','events_url','expires_at'}
or payload['events_url'] != EVENTS_PATH or not isinstance(payload['subscription'],str)
or not 20 <= len(payload['subscription']) <= 128 or type(payload['expires_at']) not in (int,float)
or not time.time()*1000 < payload['expires_at'] <= (time.time()+MAX_RECEIPT_TTL+RECEIPT_CLOCK_SKEW)*1000): return 'invalid_receipt'
scope = opaque('public-watch',key,payload['subscription'])
if not self.ledger.bind_epoch(scope,correlation): return 'receipt_replay_mismatch'
with self.lock:
if scope in self.watches: return 'already_watching'
if len(self.watches) >= 256: return 'capacity_exhausted'
stop = threading.Event()
self.watches[scope] = stop
threading.Thread(target=self._watch,args=(dict(payload),correlation,key,scope,stop),daemon=True).start()
return 'subscribing'
def close(self):
with self.lock:
for stop in self.watches.values(): stop.set()
def _watch(self, p, c, key, scope, stop):
# These are local labels only. TLS+same-key authenticated ready attests
# receipt validity; server owns literal tenant/session/history mapping.
binding = Binding(opaque('member',key),scope,opaque('history-receipt',p['subscription']),c.connection_id,c.thread_id,c.generation)
deadline = p['expires_at']/1000
try:
backoff = 1
while not stop.is_set() and time.time() < deadline and self.live(c):
conn = (http.client.HTTPSConnection(self.origin.hostname,self.origin.port,context=ssl.create_default_context(),timeout=10)
if self.origin.scheme == 'https' else http.client.HTTPConnection(self.origin.hostname,self.origin.port,timeout=10))
finished = threading.Event()
transport, resp, guard_thread = None, None, None
def interrupt_transport():
# getresponse may detach conn.sock for HTTP/1.0 or
# Connection: close. Keep our actual socket until cleanup.
if transport is not None:
try: transport.shutdown(socket.SHUT_RDWR)
except OSError: pass
def guard():
while not finished.wait(.05):
if stop.is_set() or time.time() >= deadline or not self.live(c):
stop.set()
interrupt_transport()
return
try:
# Bound TCP/TLS connection establishment separately from
# headers/stream reads: quiet research has no idle deadline.
conn.connect()
transport = conn.sock
transport.settimeout(None)
if stop.is_set() or time.time() >= deadline or not self.live(c): break
guard_thread = threading.Thread(target=guard,daemon=True)
guard_thread.start()
body = {'subscription':p['subscription']}
cursor = self.ledger.cursor(scope)
if cursor: body['cursor'] = cursor
conn.request('POST', EVENTS_PATH, json.dumps(body), {'Authorization':'Bearer '+key,'Content-Type':'application/json'})
resp = conn.getresponse()
if resp.status != 200:
if resp.status not in (408,429) and not 500 <= resp.status < 600: break
elif 'text/event-stream' not in resp.getheader('Content-Type',''):
break
else:
parser, ready = Frames(), False
while self.live(c) and not stop.is_set() and time.time() < deadline:
chunk = resp.read1(4096)
if not chunk: break
for event,data,event_id,_ in parser.feed(chunk):
if stop.is_set() or time.time() >= deadline or not self.live(c): break
if not data: continue # keepalive
value = json.loads(data)
if event == 'ready':
if not isinstance(value,dict) or not isinstance(value.get('cursor'),str): raise ValueError('invalid ready')
ready = self.live(c) and self.mux.bind(binding)
if not ready: return
elif event == 'notice':
if not ready: raise ValueError('notice before ready')
self._notice(binding,scope,value,event_id,deadline,c)
except (OSError,ValueError,http.client.HTTPException):
pass # no credential-bearing exceptions in logs
finally:
finished.set()
interrupt_transport()
if guard_thread is not None: guard_thread.join()
if resp is not None: resp.close()
conn.close()
if transport is not None: transport.close()
if stop.is_set() or time.time() >= deadline or not self.live(c): break
if stop.wait(min(backoff,max(0,deadline-time.time()))): break
backoff = min(backoff*2,15)
finally:
with self.mux._lock:
k = (binding.tenant_id,binding.bekci_session_id)
if self.mux._bindings.get(k) == binding: self.mux._bindings.pop(k,None)
with self.lock: self.watches.pop(scope,None)
def _notice(self,b,scope,value,event_id,deadline,c):
if not self.live(c) or not isinstance(value,dict): return
n, cursor = value.get('notice'), value.get('cursor')
if (not isinstance(n,dict) or not isinstance(cursor,str) or not 0 < len(cursor) <= 512
or event_id != cursor or not isinstance(n.get('id'),str) or not 0 < len(n['id']) <= 256
or n.get('type') not in ('duzeltme','arastirma') or not isinstance(n.get('metin'),str)
or not 0 < len(n['metin']) <= 8000): return
if not self.ledger.claim(scope,n['id']):
self.ledger.ack(scope,n['id'],cursor,'replay_suppressed'); return
now = time.time()
notice = dict(notice_id=n['id'],tenant_id=b.tenant_id,bekci_session_id=b.bekci_session_id,
history_revision=b.history_revision,connection_id=b.connection_id,thread_id=b.thread_id,
expected_generation=b.generation,issued_at=now,expires_at=min(now+60,deadline),text=n['metin'])
result = self.mux.submit(notice)
self.ledger.ack(scope,n['id'],cursor,result['status'])
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,163 @@
"""Provider-neutral history preparation. Never edits the saved Codex transcript.
Inference routing and voice are not responsibilities of this module. A caller
supplies the target window and an AITURK summarizer; subscription-only requests
must not enter the budget preparation path.
"""
import copy
import hashlib
import json
import threading
from collections import OrderedDict
import ozet_kopru
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
"function_call": "fc", "function_call_output": "fc"}
RATIO = 0.70
class TransferError(ValueError):
pass
def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def tokens(value):
# Deliberately more conservative than the old 3.2 bytes/token estimate.
return (len(encoded(value).encode("utf-8")) + 1) // 2
def input_budget(req, window):
return min(int(window * RATIO), window - max(1024, int(req.get("max_output_tokens") or 0)))
def foreign(req):
for item in req.get("input") or []:
if not isinstance(item, dict):
continue
identity = str(item.get("id") or "")
want = PREFIXES.get(item.get("type"))
if ("chatcmpl-" in identity or "astrabridge" in identity
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET)
or (want and identity and not identity.startswith(want + "_"))):
return True
return False
def normalize(req):
"""Repair local wrappers/IDs, retaining real OpenAI encrypted compaction.
Only foreign server references (not inline tool results) are removed. Their
visible summary/text, when supplied, is retained as ordinary context.
"""
out = copy.deepcopy(req)
items = out.get("input")
if not isinstance(items, list):
return out
result = []
for item in items:
if not isinstance(item, dict):
result.append(item)
continue
encrypted = item.get("encrypted_content")
if item.get("type") == "compaction" and isinstance(encrypted, str) and encrypted.startswith(ozet_kopru.ISARET):
text = ozet_kopru.ozet_metni(item)
if not text:
raise TransferError("Eski AITURK özeti okunamadı; geçmiş değiştirilmedi.")
result.append(ozet_kopru._ozet_mesaji(text))
continue
identity = item.get("id")
if "chatcmpl-" in str(identity) and item.get("type") in ("reasoning", "item_reference"):
visible = [p.get("text", "") for p in item.get("summary", []) if isinstance(p, dict)]
if any(visible):
result.append(ozet_kopru._ozet_mesaji("\n".join(visible)))
continue
prefix = PREFIXES.get(item.get("type"))
if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"):
item["id"] = prefix + "_" + identity.split("_", 1)[-1]
result.append(item)
out["input"] = result
return out
def split_history(items, keep_users=2):
"""Keep the last two user turns and complete tool pairs; never split a call.
System/developer items are kept verbatim regardless of their position.
"""
users = [i for i, x in enumerate(items) if isinstance(x, dict) and x.get("role") == "user"]
cut = users[-keep_users] if len(users) >= keep_users else (users[-1] if users else len(items))
# Some clients put a user/hook message between a call and its result.
while cut:
tail_calls = {x.get("call_id") for x in items[cut:] if isinstance(x, dict) and x.get("call_id")}
earlier = [i for i, x in enumerate(items[:cut]) if isinstance(x, dict) and x.get("call_id") in tail_calls]
if not earlier:
break
cut = min(earlier)
protected, old, recent = [], [], []
for i, item in enumerate(items):
if isinstance(item, dict) and item.get("role") in ("system", "developer"):
protected.append(item)
elif i < cut:
old.append(item)
else:
recent.append(item)
return protected, old, recent
class Preparer:
def __init__(self):
self.cache = OrderedDict()
self.lock = threading.Lock()
def prepare(self, request, window, summarize, scope="", force=False):
"""Summarize old history to fit 70%, leaving the original untouched.
summarize(text, max_tokens) must return a completed semantic summary or
raise. Tool execution is forbidden for that separate model request.
Cache keys include the caller scope, exact history and available budget.
"""
req = normalize(request)
window = int(window)
if window < 1024:
raise TransferError("Hedef modelin bağlam kapasitesi doğrulanamadı.")
limit = input_budget(req, window)
if not force and tokens(req) <= limit:
return req
protected, old, recent = split_history(req.get("input") or [])
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
# Preserve the last user turn even if keeping two turns is too expensive.
if available < 1024 or not old:
protected, old, recent = split_history(req.get("input") or [], keep_users=1)
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
if available < 512 or not old:
raise TransferError("Son mesaj ve zorunlu talimatlar hedefe sığmıyor; geçmiş korundu, modele eksik içerik gönderilmedi.")
# Encrypted items must be rendered by their owner, never summarized as ciphertext.
if any(isinstance(x, dict) and x.get("encrypted_content") for x in old):
raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.")
source = encoded(old)
budget = min(12000, max(512, available // 2))
key = hashlib.sha256((scope + str(budget) + source).encode()).hexdigest()
with self.lock:
summary = self.cache.get(key)
if summary is None:
summary = summarize(source, budget)
if not isinstance(summary, str) or len(summary.strip()) < 80:
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
if tokens(candidate) > limit:
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
with self.lock:
self.cache[key] = summary
self.cache.move_to_end(key)
while len(self.cache) > 32:
self.cache.popitem(last=False)
return candidate
PREPARER = Preparer()
@@ -86,6 +86,7 @@ def ide_yapilandirma(agent=AGENT):
return base.rstrip("/"), varsayilan, adlar
EN_COK_MODEL = 1000
AITURK_EKI = " (AITURK)"
# Gecidin kisa saglayici kodu -> kullanicinin gormek istedigi kod (10 Ekim 2026: "agy/gemini").
_SAGLAYICI_KISA = {"ag": "agy"}
@@ -148,7 +149,11 @@ def adlandir(girdiler, dolu=(), turler=None):
for k, _ in girdiler:
ad = kisa[k] if sayac[kisa[k].lower()] == 1 else tam[k]
if ad.lower() in kullanilan:
ad = f"{ad} [{k[-8:]}]"
taban = ad
for uzunluk in range(8, len(k) + 8, 4): # son 8 karakter de cakisirsa ek uzatilir: iki satir ayni adi tasimaz
ad = f"{taban} [{k[-uzunluk:]}]"
if ad.lower() not in kullanilan:
break
kullanilan.add(ad.lower())
sonuc[k] = ad
return sonuc
@@ -184,16 +189,18 @@ def sec(data, tanimli=(), kapsam="ide", turler=None):
if taban != s["ad"] and taban.split(" · ")[-1].strip().lower() not in adlar:
adlar.add(taban.split(" · ")[-1].strip().lower())
s["ad"] = taban
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen]
return [{"id": s["id"], "ad": s["ad"], "kind": s["kind"], "yol": s["yol"]} for s in secilen[:EN_COK_MODEL]]
def sirala_codex(modeller, ekler):
def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
"""Ozgun abonelik modelleri oldugu gibi kalir: sirasi, priority degeri ve adi degismez,
listenin basinda durur. Ek modeller onlarin ARKASINA eklenir: combo A-Z, sonra
saglayici A-Z / model A-Z. Ozgun bir modelle ayni adi tasiyan ek model " (AITURK)"
ekiyle ayirt edilir; secicide ozgunun yerine gecmis gibi gorunmez.
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz."""
bilgiler = {m["slug"]: m for m in ekler}
Kimlik ve yetenek alanlari korunur; kayitli model secimine dokunulmaz.
ozgun_kimlikler: ust listeden (chatgpt.com) gelen kimlikler. Ayarda ayni kimlik ek model diye yazilmis
olsa da o model ozgundur; yerinden ve sirasindan oynamaz."""
bilgiler = {m["slug"]: m for m in ekler if m["slug"] not in ozgun_kimlikler}
ozgunler = [m for m in modeller if m.get("slug", "") not in bilgiler]
ek_modeller = [m for m in modeller if m.get("slug", "") in bilgiler]
def yalin(ad): # "GPT-6-1-Sol" ile "GPT-6.1-Sol" ayni ad sayilir
@@ -348,5 +355,6 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
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,57 @@
"""Ek modellere internet aramasi (11 Ekim 2026).
Kullanici karari (11 Ekim 2026, sesli): "Arastirma yapabilmesini istiyorum. Ayni Codex'te yapabildikleri gibi olmasini
istiyorum." Codex'in yerlesik web aramasi OpenAI sunucusunda calistigi icin ek modele verilemez (kopru onu arac
listesinden atar); olcum: ek model arama araci bulamayinca turu bos bitirdi. Kopru bunun yerine ek modele giden
istege, aramayi kabuk araciyla yapan hazir komutu (ara.py) soyleyen kisa bir not ekler. Codex'in kendi modelleri bu
notu hic gormez: onlarin istegi kopruye ugramaz.
Ayar (bridge.json, istege bagli): "internet": {"enabled": false} kapatir; {"talimat": "..."} metni degistirir.
"""
import os
import sys
ISARET = "[İnternet araması]"
VARSAYILAN = (
ISARET + " Bu modelde yerleşik web araması yok; internetten bilgi gerektiğinde (güncel olay, araştırma, kaynak, "
"fiyat, \"araştır\" isteği) kabuk aracınla şu iki komutu kullan:\n"
"Arama: {ara} \"sorgu\"\n"
"Sayfa okuma: {oku} <adres>\n"
"Önce ara, sonra işe yarayan iki üç sonucu oku, cevabında kaynak adreslerini belirt. Başka bir arama aracı "
"arama (tool_search ile bulunmaz). Arama hizmeti engeli veya ayrıştırma hatasında sorguları tekrar tekrar "
"değiştirme; aramanın yapılamadığını açıkça söyle. Gerçek sonuç yoksa en fazla bir farklı sorgu dene. Bilgi uydurma."
)
def python_yolu(calisan=None):
"""Modelin calistiracagi yorumlayici. Kopru penceresiz (pythonw) calisiyorsa cikti veren python secilir."""
calisan = calisan or sys.executable or "python"
klasor, ad = os.path.split(calisan)
if ad.lower() == "pythonw.exe":
aday = os.path.join(klasor, "python.exe")
if os.path.isfile(aday):
return aday
return calisan
def talimat(ayar, betik, python=None):
"""Eklenecek metin; kapaliysa ya da betik yoksa None."""
ayar = ayar if isinstance(ayar, dict) else {}
if ayar.get("enabled") is False or not betik or not os.path.isfile(betik):
return None
on = f'& "{python or python_yolu()}" "{betik}"' if os.name == 'nt' else f'"{python or python_yolu()}" "{betik}"'
return str(ayar.get("talimat") or VARSAYILAN).replace("{ara}", on + " ara").replace("{oku}", on + " oku")
def ekle(req, metin):
"""Notu 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[: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 ISARET in yazi:
return False
girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": metin}]})
return True
@@ -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,22 @@
{
"enabled": true,
"endpoint": "https://api.typesafe.ai/v1/systemone",
"model": "jev-1.13.0",
"fallbackModel": "Katman-1",
"rejectionRules": [
{ "from": "Katman-1", "to": "Katman-2" },
{ "from": "Katman-2", "to": "Katman-3" },
{ "from": "Katman-3", "to": "Katman-4" }
],
"exemptModels": ["Sesli-Hizli-Zeki", "Katman-4"],
"thresholds": { "ret": 0.5, "sart": 0.7 },
"questions": {
"ret": "The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic.",
"sart": "The assistant changed the request on its own: it added its own conditions, softened or relabeled what was asked, or left part of it out, instead of doing exactly what the user asked."
},
"timeoutSeconds": 3600,
"criteria": { "ret": { "true": "", "false": "" }, "sart": { "true": "", "false": "" } },
"maxConcurrent": 0,
"queueWaitSeconds": 3600,
"failureAction": "pass"
}
@@ -0,0 +1,218 @@
"""Katman denetimi: Jev, katman combosunun her adimdaki cevabini gecirir ya da gecirmez (11 Ekim 2026).
Kullanici karari (11 Ekim 2026): "Jev kati olsun. Af yok istisna yok. Herhangi bir model Jev'e gelirse Jev bunu
gecirmezse 1.Katmana yonlendirecek. 1.Katman combosu gecmezse 2.Katmana, 2.Katman gecmezse 3.Katmana, 3.Katman
gecmezse 4.Katmana. 4.Katman combosu gelirse Jev bunu sorgulamadan gecirecek."
12 Ekim 2026 istisnasi: çözülmüş hedefi tam Sesli-Hizli-Zeki olan cevap Jev'e sorulmaz.
Taşıma/boş yanıt kurtarması ve yedek katmanların denetimi korunur.
Diğer hedeflerde denetim secili modelin katman olup olmamasina bakmaz.
Gecmezse adimi sirayla Katman-1 -> Katman-2 -> Katman-3 -> Katman-4 yapar; secili model zaten bir katmansa
yalniz ondan sonrakiler denenir. Katmanlar OmniRoute combolaridir (KATMAN); kopru onlari yalniz adiyla cagirir.
Jev (TypeSafe System One) metin uretmez, sorulan evet/hayir sorularina olasilik verir. Kural tektir ve istisnasi
yoktur: cevap istegi yapmiyorsa (ret) ya da sormadan kendi sartlariyla degistiriyorsa (sart) adim gecmez.
Metni olmayan (yalniz arac cagiran) adimda sorulacak cevap yoktur. Jev'e ulasilamazsa cevap oldugu gibi verilir.
"""
import json
import os
import re
import urllib.request
import jev_settings
ADRES = "https://api.typesafe.ai/v1/systemone"
MODEL = "jev-1.13.0"
KATMAN = ("Katman-1", "Katman-2", "Katman-3", "Katman-4")
ESIK = {"ret": 0.5, "sart": 0.7}
SORU = {
"ret": ("The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, "
"said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic."),
"sart": ("The assistant changed the request on its own: it added its own conditions, softened or relabeled what was "
"asked, or left part of it out, instead of doing exactly what the user asked."),
}
_ANAHTAR = {"okundu": False, "deger": ""}
def muaf_mi(hedef):
"""Settings' always-pass list matches exact resolved model IDs.
Taşıma hatalarının katman sırasını değiştirmez; yedek model kendi adına denetlenir.
"""
return hedef in jev_settings.read()[0]['exemptModels']
def sonrakiler(hedef):
"""Model-specific rejection edges take precedence over the general fallback.
Stop at an always-pass model or before revisiting any target.
"""
settings = jev_settings.read()[0]
ad = str(hedef or "")
if not settings['enabled'] or not ad:
return []
routes = {r['from']: r['to'] for r in settings['rejectionRules']}
result, seen = [], {ad}
# An exempt model can still use the general fallback on transport failure.
# Repeated targets stop the walk even if a file was edited outside Settings.
while len(result) < 65:
target = routes.get(ad, settings['fallbackModel'])
if target in seen:
break
seen.add(target)
result.append(target)
if target in settings['exemptModels']:
break
ad = target
return result
def anahtar(ortam=None, dosyalar=None):
"""TypeSafe anahtari: ortam degiskeni, sonra ~/.typesafe/typesafe.env. Yoksa ""."""
if ortam is None and dosyalar is None:
saved = jev_settings.key()
if saved:
return saved
ortam = os.environ if ortam is None else ortam
deger = str(ortam.get("TYPESAFE_API_KEY") or "").strip()
if deger:
return deger
if dosyalar is None:
if _ANAHTAR["okundu"]:
return _ANAHTAR["deger"]
dosyalar = [os.path.join(os.path.expanduser("~"), ".typesafe", "typesafe.env")]
onbellek = True
else:
onbellek = False
for yol in dosyalar:
try:
with open(yol, encoding="utf-8", errors="replace") as f:
for satir in f:
m = re.match(r"^\s*TYPESAFE_API_KEY\s*=\s*(.*)$", satir)
if m:
deger = m.group(1).strip().strip('"').strip("'")
break
except OSError:
continue
if deger:
break
if onbellek:
_ANAHTAR.update(okundu=True, deger=deger)
return deger
def _metin(oge):
icerik = oge.get("content")
if isinstance(icerik, str):
return icerik
return "".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
def son_istek(req):
"""Kullanicinin bu turdaki istegi: son kullanici mesaji. Sesli aktarimda yalniz <input> icindeki soz;
baska sarmalli sistem mesajlari (kanca baglami) atlanir."""
for oge in reversed(req.get("input") or []):
if not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
continue
metin = _metin(oge).strip()
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
if m:
return m.group(1).strip()
if metin and not metin.startswith("<"):
return metin
return ""
def hazirlik_mi(govde):
"""Araç çağrısı veya commentary görüldüyse adım nihai cevap değildir.
Tam SSE satırları üzerinden bakılır; parçalı olay bir sonraki parçada tamamlanır.
Araç argümanında geçen olay adı hazırlık sayılmaz.
"""
for satir in govde.splitlines():
if not satir.startswith(b'data:'):
continue
try:
olay = json.loads(satir[5:])
except (ValueError, UnicodeDecodeError):
continue
if not isinstance(olay, dict):
continue
tip = str(olay.get('type') or '')
oge = olay.get('item') or {}
if tip in ('response.output_item.added', 'response.output_item.done') and isinstance(oge, dict):
if str(oge.get('type') or '').endswith('_call') or oge.get('phase') == 'commentary':
return True
if tip in ('response.function_call_arguments.delta', 'response.function_call_arguments.done',
'response.custom_tool_call_input.delta', 'response.custom_tool_call_input.done'):
return True
return False
def komutlar(govde):
"""Yazisi olmayan adimda Jev'e giden metin: modelin bu adimda cagirdigi araclar (ad ve arguman).
govde: ust kaynagin SSE yaniti (bayt). Arac cagrisi yoksa ""."""
satirlar = []
for satir in govde.splitlines():
if not satir.startswith(b"data:") or b"response.output_item.done" not in satir:
continue
try:
oge = json.loads(satir[5:]).get("item") or {}
except (ValueError, AttributeError):
continue
if isinstance(oge, dict) and str(oge.get("type") or "").endswith("_call"):
arguman = oge.get("arguments") or oge.get("input") or oge.get("action") or ""
if not isinstance(arguman, str):
arguman = json.dumps(arguman, ensure_ascii=False)
satirlar.append(f"[komut] {oge.get('name') or oge.get('type')}: {arguman}")
return "\n".join(satirlar)
def _gonder(govde, key, settings=None):
settings = settings or jev_settings.read()[0]
istek = urllib.request.Request(settings['endpoint'], data=json.dumps(govde).encode("utf-8"), method="POST",
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
with urllib.request.urlopen(istek, timeout=settings['timeoutSeconds']) as yanit:
return json.load(yanit)
class Puan(dict):
"""Carry the thresholds used for this evaluation through a settings change."""
pass
def sor(istek, cevap, key, gonder=_gonder):
"""Jev'e sorar. Donus: {"ret", "sart"} olasiliklari | None (sorulamadi)."""
settings = jev_settings.read()[0]
govde = {"model": settings['model'], "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in settings['questions'].items()}}
for name, criteria in settings.get('criteria', {}).items():
values = {k: v for k, v in criteria.items() if v.strip()}
if values:
govde['questions'][name]['criteria'] = values
if not jev_settings.acquire(settings):
return None
try:
response = gonder(govde, key, settings) if gonder is _gonder else gonder(govde, key)
yanitlar = (response or {}).get("answers") or {}
result = Puan({k: float(yanitlar[k]["noul"]) for k in SORU})
if any(not 0 <= v <= 1 for v in result.values()):
return None
result.thresholds = settings['thresholds']
return result
except Exception:
return None
finally:
jev_settings.release()
def karar(puan):
"""True: adim gecmez, sonraki katman yapar. Istisna yok."""
thresholds = getattr(puan, 'thresholds', None) or jev_settings.read()[0]['thresholds']
return puan["ret"] >= thresholds["ret"] or puan["sart"] >= thresholds["sart"]
def hata_gecis():
return jev_settings.read()[0]['failureAction'] == 'next'
def etkin_mi():
return bool(jev_settings.read()[0]['enabled'])
@@ -0,0 +1,89 @@
"""Per-user settings, re-read for new evaluations without restarting active chats."""
import base64
import copy
import ctypes
import json
import os
import sys
import threading
import time
from pathlib import Path
DEFAULTS = json.loads(Path(__file__).with_name('jev-settings-defaults.json').read_text(encoding='utf-8'))
_condition = threading.Condition()
_active = 0
def home():
if '--home' in sys.argv:
return Path(sys.argv[sys.argv.index('--home') + 1])
return Path(os.environ.get('AITURK_CODEX_BRIDGE_HOME') or Path(__file__).parent)
def read():
try:
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
except FileNotFoundError:
return copy.deepcopy(DEFAULTS), None
settings = data['settings']
# A corrupted file must not silently change routing or contact another host.
from urllib.parse import urlsplit
url = urlsplit(settings['endpoint'])
if url.scheme != 'https' or not url.netloc or url.username or url.password:
raise ValueError('Invalid Jev endpoint')
for name in ('ret', 'sart'):
n = settings['thresholds'][name]
if not isinstance(n, (int, float)) or not 0 <= n <= 1 or not settings['questions'][name]:
raise ValueError('Invalid Jev rule')
for name in ('timeoutSeconds', 'queueWaitSeconds'):
if not isinstance(settings[name], int) or not 1 <= settings[name] <= 3600:
raise ValueError('Invalid Jev timeout')
if not isinstance(settings['maxConcurrent'], int) or not 0 <= settings['maxConcurrent'] <= 512:
raise ValueError('Invalid Jev concurrency')
if not settings['fallbackModel'] or not isinstance(settings['rejectionRules'], list) or settings['failureAction'] not in ('pass', 'next'):
raise ValueError('Invalid Jev routing')
return settings, data.get('protectedKey')
def key():
_, encrypted = read()
if not encrypted:
return ''
if os.name != 'nt':
raise RuntimeError('Windows credential protection required')
from ctypes import wintypes
class Blob(ctypes.Structure):
_fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
raw = base64.b64decode(encrypted, validate=True)
buf = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
source, result = Blob(len(raw), buf), Blob()
decrypt = ctypes.windll.crypt32.CryptUnprotectData
decrypt.argtypes = [ctypes.POINTER(Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(Blob)]
decrypt.restype = wintypes.BOOL
if not decrypt(ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)):
raise ctypes.WinError()
try:
return ctypes.string_at(result.pbData, result.cbData).decode('utf-8')
finally:
ctypes.windll.kernel32.LocalFree.argtypes = [ctypes.c_void_p]
ctypes.windll.kernel32.LocalFree(result.pbData)
def acquire(settings):
global _active
deadline = time.monotonic() + settings['queueWaitSeconds']
with _condition:
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
_condition.wait(remaining)
_active += 1
return True
def release():
global _active
with _condition:
_active -= 1
_condition.notify_all()
File diff suppressed because it is too large. Load diff
+151 -17
View File
@@ -15,23 +15,38 @@ Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday v
"""
import json
import os
import collections
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")
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
# Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir.
FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim",
"degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim",
"cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"}
OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme")
# "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir.
SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana",
"modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"}
SORU = ("hangi", "hangisi", "kim", "kimdir")
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"}
"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",
@@ -94,18 +109,68 @@ def son_kullanici_metni(req):
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 = re.findall(r"[a-z0-9]+", duz(metin))
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
kelimeler = _harfleri_birlestir(re.findall(r"[a-z0-9]+", duz(metin)))
if not kelimeler or len(kelimeler) > EN_COK_KELIME:
return None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"}
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
if fiil:
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
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} if ad else None
return {"tur": "gec", "ad": ad, "kesin": False} if len(ad) >= 2 else None
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None
if any(k.startswith(OLUMSUZ) for k in kelimeler):
return None
fiil = [k for k in kelimeler if k in FIILLER]
if fiil:
son_fiil = max(i for i, k in enumerate(kelimeler) if k in FIILLER)
if all(k in DEVAM_EKI for k in kelimeler[son_fiil + 1:]):
kelimeler = kelimeler[:son_fiil + 1]
kelimeler = _sayilari_cevir(kelimeler)
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil and k not in DOLGU]
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
# Ad soylenmemis ("modeli değiştir", "başka modele geç"): degisiklik yok, nasil soylenecegi anlatilir.
return {"tur": "gec", "ad": ad, "kesin": True} if ad else {"tur": "nasil"}
return None
@@ -113,7 +178,11 @@ 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()
if len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit():
return True
# Yarim soylenen ya da yarim yaziya dokulen soz adin basidir: "think" -> thinking, "obli" -> obliterated
# (olcum 11 Ekim 2026: "qwen 3.8 27b obli think" uc kez bulunamadi).
return len(sorgu) >= 4 and sorgu.isalpha() and parca.startswith(sorgu)
def eslestir(ad, modeller):
@@ -131,15 +200,41 @@ def eslestir(ad, modeller):
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
puanli.append((tutan, -len(parcalar), slug, gorunen))
kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")]
hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0
puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi))
if not puanli:
return None
en = max(p[0] for p in puanli)
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
return None # sozun cogu hicbir modele uymuyor: komut degil
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
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]]}
# Adaylardan biri digerlerinin "fazladan sozcuklu" hali degilse sorulur. Biri digerlerinin govdesiyse
# ("...-obliterated" ile "...-obliterated:thinking") soylenmeyen eki tasimayan secilir: kullanici "thinking"
# demediyse dusunmeyeni istiyordur (olcum 11 Ekim 2026). Fark surum rakamiysa tahmin edilmez, sorulur.
sayim = [(collections.Counter(parcala(p[3]) or parcala(p[2])), p) for p in iyi]
sayim.sort(key=lambda c: sum(c[0].values()))
govde, secilen = sayim[0]
if all(not (govde - c) and (c - govde) and not any(k.isdigit() for k in (c - govde)) for c, _ in sayim[1:]):
return {"slug": secilen[2], "ad": secilen[3]}
return {"adaylar": [p[3] for p in iyi[:4]]}
@@ -176,14 +271,19 @@ def metni_degistir(req, sira, yeni):
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
def gecmisi_temizle(req, modeller):
def gecmisi_temizle(req, modeller, etkin=None):
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
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"):
@@ -194,12 +294,42 @@ def gecmisi_temizle(req, modeller):
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. Codex'in kendi modellerine (listenin başındaki abonelik modelleri) "
"sesle geçilmez: kullanıcı onlardan birine geçmek isterse ekrandaki model seçiciden seçip tek başına \".\" "
"yazması gerektiğini söyle.")
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. Codex'in kendi "
"modellerine sesle geçilmez; onlar için ekrandaki model seçiciden seçip tek başına \".\" yaz.")
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. "
@@ -244,6 +374,10 @@ class Yonlendirme:
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):
@@ -0,0 +1,573 @@
"""Owned native Codex stdio adapter; inert until explicitly launched by an owner.
Only a trusted in-process root adapter may install Binding objects. There is no
HTTP bind/RPC endpoint, tenant inference, or activated SSE subscription.
appendSpeech acceptance is not proof of audible speech or history persistence.
"""
from __future__ import annotations
import hmac
import hashlib
import json
import queue
import secrets
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import BinaryIO, TypedDict, Callable
@dataclass(frozen=True)
class WatchReceipt:
"""INTERNAL adapter contract, not a claim about backend wire field names.
Backend resolver authenticates an opaque handle using the member credential
against a trusted, configured Omni endpoint. Backend secret stays server-side.
The resolver must verify tenant, source request thread and history together.
No URL or identity supplied by a notice is trusted as subscription authority.
"""
opaque_handle: str
tenant_id: str
bekci_session_id: str
history_revision: str
thread_id: str
expires_at: float
@dataclass(frozen=True)
class RequestCorrelation:
"""Captured by the owned HTTP bridge from the actual incoming request.
Must be captured before metadata arrives, not reconstructed from its body.
generation is the mux epoch observed at request capture, never the latest tab.
"""
connection_id: str
thread_id: str
generation: int
class WatchReceiptAdapter:
"""One consumer; root chooses transport/decoder after backend agreement.
decode extracts only the opaque handle from bekci.watch SSE data or the
nonstream _bekci_watch value. resolve performs authenticated backend lookup.
Missing agreement is fail-closed, not fabricated ready metadata.
"""
def __init__(self, mux, *, decode: Callable[[object], str] | None = None,
resolve: Callable[[str], WatchReceipt] | None = None):
self.mux, self.decode, self.resolve = mux, decode, resolve
self._accepted = {}
def capture(self, headers: dict, body: dict) -> RequestCorrelation | None:
normalized = {k.lower(): v for k, v in headers.items()}
values = [normalized.get('session-id'), normalized.get('thread-id'), body.get('prompt_cache_key')]
if not all(isinstance(v, str) and v for v in values) or len(set(values)) != 1:
return None
snap = self.mux.snapshot()
state = snap['threads'].get(values[0], {})
if snap['closed'] or not state.get('active'):
return None
return RequestCorrelation(snap['connection_id'], values[0], state['generation'])
def accept(self, transport: str, payload: object, correlation: RequestCorrelation | None) -> str:
if transport not in ('bekci.watch', '_bekci_watch'):
return 'unsupported_metadata_transport'
if self.decode is None or self.resolve is None:
return 'backend_contract_not_established'
if correlation is None or correlation.connection_id != self.mux.connection_id:
return 'request_correlation_missing'
try:
handle = self.decode(payload)
if not isinstance(handle, str) or not 1 <= len(handle) <= 4096:
return 'invalid_receipt'
receipt = self.resolve(handle)
except Exception:
return 'receipt_verification_failed' # never expose credential-bearing exception text
if not isinstance(receipt, WatchReceipt) or receipt.opaque_handle != handle:
return 'invalid_receipt'
if (receipt.thread_id != correlation.thread_id or type(receipt.expires_at) not in (int, float)
or not time.time() < receipt.expires_at <= time.time() + 3600
or not all(isinstance(v, str) and 0 < len(v) <= 256 for v in
(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision, receipt.thread_id))):
return 'receipt_mismatch_or_expired'
binding = Binding(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision,
correlation.connection_id, receipt.thread_id, correlation.generation)
previous = self._accepted.get(handle)
if previous is not None and previous != (receipt, binding):
return 'receipt_replay_mismatch'
if previous is None and len(self._accepted) >= 4096:
return 'capacity_exhausted'
if not self.mux.bind(binding):
return 'stale_generation'
self._accepted[handle] = (receipt, binding)
return 'bound'
def notice(self, handle: str, notice: Notice) -> dict:
accepted = self._accepted.get(handle)
if accepted is None:
return {'status': 'binding_not_established'}
receipt, binding = accepted
if time.time() >= receipt.expires_at:
return {'status': 'receipt_expired'}
expected = dict(tenant_id=binding.tenant_id, bekci_session_id=binding.bekci_session_id,
history_revision=binding.history_revision, thread_id=binding.thread_id,
connection_id=binding.connection_id, expected_generation=binding.generation)
if not isinstance(notice, dict) or any(notice.get(k) != v for k, v in expected.items()):
return {'status': 'binding_mismatch'}
return self.mux.submit(notice)
def verified_original(path: str, expected_sha256: str) -> str:
"""Owner-pinned absolute original; no PATH/override discovery or extra engine."""
candidate = Path(path)
if not candidate.is_absolute() or not candidate.is_file() or candidate.resolve() == Path(__file__).resolve():
raise ValueError('original executable must be an explicitly pinned absolute file')
if len(expected_sha256) != 64:
raise ValueError('original executable hash required')
with candidate.open('rb') as stream:
actual = hashlib.file_digest(stream, 'sha256').hexdigest()
if not hmac.compare_digest(actual, expected_sha256.lower()):
raise ValueError('original executable hash mismatch')
return str(candidate.resolve())
def stdio_invocation(args: list[str]) -> bool:
"""Installed Desktop tc()/Ks() grammar, preserving every original argument."""
seen, i = False, 0
while i < len(args):
arg = args[i]
if arg in ('-c', '--config') and i + 1 < len(args):
i += 2
continue
if arg == 'app-server' and not seen:
seen = True
elif seen and arg in ('--stdio', '--analytics-default-enabled'):
pass
elif seen and arg == '--listen' and args[i+1:i+2] == ['stdio://']:
i += 1
else:
return False
i += 1
return seen
def launch_original(path: str, expected_sha256: str, args: list[str], *,
attach: Callable[[WatchReceiptAdapter], None] | None = None) -> int:
"""Source launcher entry for a future normal bridge module copy.
No activation/config mutation. Unknown invocation grammar passes native stdio
directly. Owner callback wires only this consumer after backend agreement.
No second engine, binary patch, token file or discovery of current UI tab.
"""
original = verified_original(path, expected_sha256)
observer = stdio_invocation(args)
mux = NativeSpeechMux([original, *args], observer=observer)
if observer and attach is not None:
try:
attach(WatchReceiptAdapter(mux))
except Exception:
# Optional adapter failure cannot take down native stdio. Invalidate
# any partial setup so no correction can escape a failed attachment.
mux._disabled = True
mux._bindings.clear()
return mux.run()
class Notice(TypedDict):
notice_id: str
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
expected_generation: int
issued_at: float
expires_at: float
text: str
@dataclass(frozen=True)
class Binding:
"""Local scope labels (legacy field names), NOT literal server identity claims.
Trusted owner attests this opaque receipt scope belongs to this native epoch.
Install only from independently verified mapping, NEVER from notice fields.
Rebind explicitly for a new history revision or realtime generation.
"""
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
generation: int
def _object(raw: bytes) -> dict:
try:
value = json.loads(raw)
return value if isinstance(value, dict) else {}
except (ValueError, UnicodeError):
return {}
def _id(value):
return (type(value), value) if type(value) in (str, int) else None
class NativeSpeechMux:
"""One original process, bounded FIFO, one writer in each direction.
argv is the original executable plus its original arguments. observer=True
must only be selected for a verified stdio app-server invocation. The CLI
deliberately recognizes only the exact ['app-server'] invocation; all other
invocations inherit the original stdio directly. No environment/cwd rewrite.
"""
def __init__(self, argv: list[str], *, observer: bool = False):
self.argv = list(argv)
self.observer = observer
self.connection_id = secrets.token_hex(24)
self._prefix = 'bekci-speech-' + secrets.token_hex(24) + ':'
self._token = secrets.token_urlsafe(32)
self._lock = threading.RLock()
self._queue = queue.Queue(maxsize=32)
self._ended = threading.Event()
self._eof = False
self._disabled = False
self._threads = {}
self._starts = set()
self._bindings = {}
self._receipts = {}
self._pending = {}
self._counter = 0
self._server = None
self._process = None
def snapshot(self) -> dict:
with self._lock:
return {'connection_id': self.connection_id,
'binding_status': 'established' if self._bindings else 'binding_not_established',
'threads': {k: dict(v) for k, v in self._threads.items()},
'closed': self._eof or self._ended.is_set(),
'injection_disabled': self._disabled}
def bind(self, binding: Binding) -> bool:
"""Trusted local API only. No untrusted endpoint can invoke this."""
with self._lock:
st = self._threads.get(binding.thread_id, {})
if (binding.connection_id != self.connection_id or not st.get('active')
or st.get('generation') != binding.generation or self._eof
or self._ended.is_set()):
return False
self._bindings[(binding.tenant_id, binding.bekci_session_id)] = binding
return True
def _invalidate(self, thread):
if not isinstance(thread, str):
return
if thread in self._threads:
self._threads[thread]['active'] = False
self._bindings = {k: b for k, b in self._bindings.items() if b.thread_id != thread}
def _observe_client(self, obj):
with self._lock:
rid = _id(obj.get('id'))
# Responses to server requests may legitimately reuse our ID.
if 'method' in obj and isinstance(obj.get('id'), str) and obj['id'].startswith(self._prefix):
self._disabled = True
self._pending.pop(obj['id'], None)
if obj.get('method') in ('thread/start', 'thread/resume', 'thread/fork') and rid:
if len(self._starts) < 4096:
self._starts.add(rid)
else:
self._disabled = True
if obj.get('method') in ('thread/realtime/stop', 'thread/realtime/start', 'thread/unsubscribe'):
params = obj.get('params')
if isinstance(params, dict):
self._invalidate(params.get('threadId'))
def _observe_server(self, obj) -> bool:
with self._lock:
rid = obj.get('id')
response = 'method' not in obj and (('result' in obj) != ('error' in obj))
if response and isinstance(rid, str) and rid in self._pending:
key = self._pending.pop(rid)
self._receipts[key]['status'] = 'rpc_error' if 'error' in obj else 'rpc_accepted'
return True
if response and _id(rid) in self._starts:
self._starts.discard(_id(rid))
result = obj.get('result')
thread = result.get('thread', {}) if isinstance(result, dict) else {}
tid = thread.get('id') if isinstance(thread, dict) else None
if isinstance(tid, str):
self._threads.setdefault(tid, {'generation': 0, 'active': False, 'realtime_session_id': None})
params = obj.get('params')
if not isinstance(params, dict) or 'id' in obj:
return False
tid = params.get('threadId')
if not isinstance(tid, str):
return False
method = obj.get('method')
if method == 'thread/realtime/started':
self._invalidate(tid)
previous = self._threads.get(tid, {}).get('generation', 0)
self._threads[tid] = {'generation': previous + 1, 'active': True,
'realtime_session_id': params.get('realtimeSessionId')}
elif method in ('thread/realtime/closed', 'thread/realtime/error'):
self._invalidate(tid)
return False
def _validate(self, notice):
if not isinstance(notice, dict) or set(notice) != set(Notice.__annotations__):
return 'invalid_schema'
for key in Notice.__annotations__:
if key not in ('expected_generation', 'issued_at', 'expires_at'):
if not isinstance(notice[key], str) or not notice[key].strip() or len(notice[key]) > (8000 if key == 'text' else 256):
return 'invalid_schema'
if type(notice['expected_generation']) is not int or notice['expected_generation'] < 1:
return 'invalid_schema'
if any(type(notice[k]) not in (int, float) for k in ('issued_at', 'expires_at')):
return 'invalid_schema'
now = time.time()
if not (now - 60 <= notice['issued_at'] <= now + 2 and now < notice['expires_at'] <= notice['issued_at'] + 60):
return 'expired_or_invalid_ttl'
if self._eof or self._ended.is_set():
return 'closed'
if self._disabled or not self.observer:
return 'injection_disabled'
binding = self._bindings.get((notice['tenant_id'], notice['bekci_session_id']))
if binding is None:
return 'binding_not_established'
if binding != Binding(notice['tenant_id'], notice['bekci_session_id'], notice['history_revision'],
notice['connection_id'], notice['thread_id'], notice['expected_generation']):
return 'binding_mismatch'
st = self._threads.get(binding.thread_id, {})
if not st.get('active') or st.get('generation') != binding.generation:
return 'stale_generation'
return None
def submit(self, notice: Notice) -> dict:
with self._lock:
error = self._validate(notice)
if error:
return {'status': error}
key = (notice['tenant_id'], notice['bekci_session_id'], notice['notice_id'])
if key in self._receipts:
return dict(self._receipts[key])
# Never evict dedupe during this process lifetime or retry uncertain sends.
if len(self._receipts) >= 4096:
return {'status': 'capacity_exhausted'}
receipt = {'status': 'queued'}
self._receipts[key] = receipt
try:
self._queue.put_nowait(('notice', (dict(notice), key)))
except queue.Full:
del self._receipts[key]
return {'status': 'busy'}
return dict(receipt)
def start_inlet(self) -> tuple[str, str]:
"""Return loopback URL and secret to trusted owner IN MEMORY; never log.
One bounded request at a time; 2s timeout applies only to inlet HTTP reads,
never the original engine. Owner must arrange private credential delivery.
"""
if not self.observer or self._server is not None:
raise ValueError('inlet requires an unused app-server observer')
mux = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def setup(self):
super().setup()
self.connection.settimeout(2)
def do_POST(self):
try:
auth = self.headers.get('Authorization', '')
if not hmac.compare_digest(auth.encode(), ('Bearer ' + mux._token).encode()):
self.send_error(401)
return
if self.headers.get('Origin') or self.headers.get('Transfer-Encoding'):
self.send_error(400)
return
lengths = self.headers.get_all('Content-Length', [])
if len(lengths) != 1 or not lengths[0].isdigit() or not 0 < int(lengths[0]) <= 65536:
self.send_error(400)
return
raw = self.rfile.read(int(lengths[0]))
if len(raw) != int(lengths[0]):
self.send_error(400)
return
if self.path == '/notice':
result = mux.submit(_object(raw))
elif self.path == '/status' and _object(raw) == {}:
result = mux.snapshot()
else:
self.send_error(404)
return
body = json.dumps(result).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
except (OSError, ValueError):
return
self._server = HTTPServer(('127.0.0.1', 0), Handler)
threading.Thread(target=self._server.serve_forever, daemon=True).start()
return f'http://127.0.0.1:{self._server.server_port}', self._token
def _put(self, item):
while not self._ended.is_set():
try:
self._queue.put(item, timeout=.1)
return True
except queue.Full:
pass
return False
def run(self, source: BinaryIO | None = None, target: BinaryIO | None = None) -> int:
if not self.observer:
return subprocess.call(self.argv) # exact inherited stdin/stdout/stderr/env/cwd
source = source if source is not None else sys.stdin.buffer
target = target if target is not None else sys.stdout.buffer
process = self._process = subprocess.Popen(self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def read_client():
fragment = False
while not self._ended.is_set():
raw = source.readline(65536)
if not raw:
with self._lock:
self._eof = True
self._put(('eof', None))
return
complete = raw.endswith(b'\n')
if not fragment and complete:
self._observe_client(_object(raw))
elif not complete:
# Oversized records still forward byte-for-byte, but cannot be
# safely observed. Fail closed for injection for this connection.
with self._lock:
self._disabled = True
fragment = not complete
if not self._put(('raw', raw)):
return
def send_notice(value):
notice, key = value
with self._lock:
error = self._validate(notice)
if error:
self._receipts[key]['status'] = error
return
self._counter += 1
rid = self._prefix + str(self._counter)
self._pending[rid] = key
self._receipts[key]['status'] = 'sent_unknown'
raw = json.dumps({'id': rid, 'method': 'thread/realtime/appendSpeech',
'params': {'threadId': notice['thread_id'], 'text': notice['text']}},
ensure_ascii=True).encode() + b'\n'
# No lock during blocking pipe I/O: observer must drain server output.
# Protocol has no server-side generation precondition; this race remains.
process.stdin.write(raw)
process.stdin.flush()
def write_server():
partial = False
try:
while not self._ended.is_set():
try:
kind, value = self._queue.get(timeout=.1)
except queue.Empty:
continue
if kind == 'eof':
return
if kind == 'notice':
if partial:
# An oversized frame has disabled injection; reject now.
with self._lock:
self._receipts[value[1]]['status'] = 'injection_disabled'
else:
send_notice(value)
else:
# Re-apply lifecycle invalidation at the actual write boundary
# too: a prior server start may have arrived while queued.
if not partial and value.endswith(b'\n'):
obj = _object(value)
if obj.get('method') in ('thread/realtime/start', 'thread/realtime/stop'):
params = obj.get('params')
if isinstance(params, dict):
with self._lock:
self._invalidate(params.get('threadId'))
process.stdin.write(value)
process.stdin.flush()
partial = not value.endswith(b'\n')
except (OSError, ValueError):
pass
finally:
try:
process.stdin.close()
except OSError:
pass
reader = threading.Thread(target=read_client, daemon=True)
writer = threading.Thread(target=write_server, daemon=True)
reader.start()
writer.start()
fragment = False
try:
while True:
raw = process.stdout.readline(65536)
if not raw:
break
complete = raw.endswith(b'\n')
consumed = not fragment and complete and self._observe_server(_object(raw))
if not complete:
with self._lock:
self._disabled = True
fragment = not complete
if not consumed:
target.write(raw)
target.flush()
finally:
self._ended.set()
process.stdout.close()
with self._lock:
self._bindings.clear()
for st in self._threads.values():
st['active'] = False
for receipt in self._receipts.values():
if receipt['status'] == 'queued':
receipt['status'] = 'closed'
if self._server:
self._server.shutdown()
self._server.server_close()
return process.wait() # no deadline on legitimate native work
def main() -> int:
# Future launcher must pin/verify the original executable, never resolve the
# override recursively. No CODEX_CLI_PATH configuration is performed here.
if len(sys.argv) >= 5 and sys.argv[1] == '--pinned-original' and sys.argv[4] == '--':
try:
return launch_original(sys.argv[2], sys.argv[3], sys.argv[5:])
except ValueError:
print('original executable verification failed', file=sys.stderr)
return 2
if len(sys.argv) < 3 or sys.argv[2] != '--':
print('usage: native_speech_mux.py --pinned-original ORIGINAL_EXECUTABLE SHA256 -- ORIGINAL_ARGS...', file=sys.stderr)
return 2
argv = [sys.argv[1], *sys.argv[3:]]
return NativeSpeechMux(argv, observer=argv[1:] == ['app-server']).run()
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,42 @@
"""Owned startup entry. No user environment/config changes or override discovery."""
import json
import os
from pathlib import Path
import subprocess
import sys
import threading
from native_speech_mux import NativeSpeechMux, stdio_invocation, verified_original
from bekci_watch import Ledger, WatchOwner
def main(manifest, args):
cfg = json.loads(Path(manifest).read_text(encoding='utf-8-sig'))
original = verified_original(cfg['original'], cfg['original_sha256'])
if not stdio_invocation(args):
return subprocess.call([original,*args])
# Existing public catalog and original model passthrough handler is reused.
# A per-child CLI setting selects the ephemeral owned bridge; no global file edit.
saved_env = dict(os.environ)
os.environ['AITURK_CODEX_BRIDGE_HOME'] = cfg['bridge_home']
os.environ['HERMES_HOME'] = cfg['hermes_home']
try:
import kopru
finally:
os.environ.clear(); os.environ.update(saved_env)
from http.server import ThreadingHTTPServer
server = ThreadingHTTPServer(('127.0.0.1',0), kopru.H)
mux = NativeSpeechMux([original,*args,'-c',f'openai_base_url="http://127.0.0.1:{server.server_port}/backend-api/codex"'],observer=True)
Path(cfg['bridge_home']).mkdir(parents=True,exist_ok=True)
owner = WatchOwner(mux,Ledger(str(Path(cfg['bridge_home'])/'native-watch-ledger.sqlite3')))
kopru.WATCH_OWNER = owner
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
return mux.run()
finally:
owner.close(); server.shutdown(); server.server_close()
if __name__ == '__main__':
try:
raise SystemExit(main(sys.argv[1],sys.argv[2:]))
except (OSError,ValueError,KeyError):
sys.stderr.write('Owned native launcher configuration/integrity failure.\n')
raise SystemExit(78)
@@ -266,10 +266,11 @@ def coz_istegi(oge, model, talimat="Sen bir konusma ozeti aktaricisisin."):
def sse_metni(govde):
"""SSE govdesinden (bayt) asistan metnini toplar."""
"""SSE govdesinden (bayt) asistan metnini toplar. Yanit tamamlanmadiysa (akis yarida kesildi) None doner:
yarim metin tam ozet diye kalici onbellege yazilmasin."""
a = OzetAkisi()
a.feed(govde if govde.endswith(b"\n\n") else govde + b"\n\n")
return a.metin if a.bitti else a._topla()
return a.metin if a.bitti else None
# ---- Dongu freni ----
@@ -300,6 +301,8 @@ class DonguFreni:
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 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
@@ -307,7 +310,7 @@ class DonguFreni:
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
+87 -11
View File
@@ -16,6 +16,7 @@ Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asma
import json
import os
import re
import threading
import time
import urllib.request
@@ -59,7 +60,7 @@ def _oku(yol):
def _yaz(yol, veri):
gecici = yol + ".tmp"
gecici = "%s.%d.tmp" % (yol, threading.get_ident()) # es zamanli iki yazma ayni gecici dosyada carpismasin
with open(gecici, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
os.replace(gecici, yol)
@@ -86,7 +87,11 @@ def nano_yukle(home, ua="AstraBridge/1.0 (codex)", timeout=20, simdi=None, getir
simdi = time.time() if simdi is None else simdi
yol = os.path.join(home, NANO_DOSYA) if home else ""
onbellek = _oku(yol) if yol else {}
if onbellek.get("modeller") and simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN:
try:
taze = simdi - float(onbellek.get("zaman") or 0) < NANO_OMUR_SN
except (TypeError, ValueError):
taze = False # bozuk "zaman" alani yenilemeyi engellemez
if onbellek.get("modeller") and taze:
return onbellek["modeller"]
if not ag:
return onbellek.get("modeller") or {}
@@ -141,12 +146,27 @@ def ogren_aracsiz(home, kimlik):
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 ""))
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:
@@ -227,6 +247,29 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
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"
@@ -243,8 +286,38 @@ KIRP_ESIK = 6000 # bundan kisa metin parcasi kirpilmaz
KIRP_BAS = KIRP_SON = 1500
def tahmini_token(req):
return int(len(json.dumps(req, ensure_ascii=False).encode("utf-8")) / BAYT_BOLU_TOKEN)
# 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):
@@ -254,8 +327,9 @@ def _metin_parcalari(req):
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"), default=-1)
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"):
@@ -271,22 +345,24 @@ def _metin_parcalari(req):
return parcalar
def sigdir(req, pencere_token):
def sigdir(req, pencere_token, oran=None):
"""Istegi hedef pencereye sigdirir. Donus: (kirpilan parca sayisi, sigdi mi, tahmini token)."""
sinir = int(pencere_token * DOLULUK)
tahmin = tahmini_token(req)
tahmin = tahmini_token(req, oran)
if not pencere_token or tahmin <= sinir:
return 0, True, tahmin
kirpilan = 0
parcalar = sorted(_metin_parcalari(req), key=lambda sa: len(sa[0][sa[1]]), reverse=True)
en_yeni = _son_arac_ciktisi(req)
# Buyukten kucuge; en yeni arac ciktisi en sona kalir (yalniz digerleri yetmezse kirpilir).
parcalar = sorted(_metin_parcalari(req), key=lambda sa: (id(sa[0]) in en_yeni, -len(sa[0][sa[1]])))
for sahip, anahtar in parcalar:
metin = sahip[anahtar]
if len(metin) < KIRP_ESIK:
break
continue
sahip[anahtar] = (metin[:KIRP_BAS] + f"\n[... {len(metin) - KIRP_BAS - KIRP_SON} karakter çıkarıldı (köprü): "
f"seçili modelin bağlam penceresine sığmıyordu ...]\n" + metin[-KIRP_SON:])
kirpilan += 1
tahmin = tahmini_token(req)
tahmin = tahmini_token(req, oran)
if tahmin <= sinir:
return kirpilan, True, tahmin
return kirpilan, tahmin <= sinir, tahmin
@@ -0,0 +1,28 @@
"""Provision a reviewable launcher candidate. Never sets CODEX_CLI_PATH or restarts apps."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
def prepare(source, destination, original, expected_sha256, python, bridge_home, hermes_home):
source, destination = Path(source).resolve(), Path(destination).resolve()
original, python = Path(original).resolve(), Path(python).resolve()
def digest(p):
with p.open('rb') as f: return hashlib.file_digest(f,'sha256').hexdigest()
if digest(original) != expected_sha256.lower(): raise ValueError('chosen official original hash mismatch')
if original == destination/'native-watch.exe': raise ValueError('recursive original')
if source == destination: raise ValueError('candidate must use separate destination')
destination.mkdir(parents=True,exist_ok=True)
for p in source.rglob('*'):
if p.is_file() and (p.suffix in ('.py','.pyd') or p.name in ('native-watch.exe','LICENSE')) and not p.name.startswith('test_'):
target=destination/p.relative_to(source);target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(p,target)
script=destination/'native_watch_launcher.py'
manifest=dict(original=str(original),original_sha256=expected_sha256.lower(),python=str(python),python_sha256=digest(python),script=str(script),script_sha256=digest(script),bridge_home=str(Path(bridge_home).resolve()),hermes_home=str(Path(hermes_home).resolve()))
(destination/'native-watch.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
return destination/'native-watch.exe'
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
for name in ('source','destination','original','expected-sha256','python','bridge-home','hermes-home'): p.add_argument('--'+name,required=True)
args=vars(p.parse_args());print(prepare(**args))
@@ -59,7 +59,8 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
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:
@@ -72,7 +73,7 @@ def resimleri_sadelestir(req, tut_bayt=TUT_BAYT, gorsel=False):
kalan = tut_bayt - sum(_boyut(p) for i in koru for p in inp[i].get('content') or [] if _medya_mi(p))
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' and it.get('role') == 'user'):
if i in koru or not (isinstance(it, dict) and it.get('type', 'message') == 'message' and it.get('role') == 'user'):
continue
icerik = it.get('content')
resimler = [p for p in icerik if _medya_mi(p) and MEDYA_TURLERI.get(p.get('type')) == 'resim'] \
@@ -10,18 +10,20 @@ import urllib.request
# Load only the actual adapter and routing method: no catalog/network/thread
# startup or credential loading is needed to exercise the production code.
source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
source = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
flatten = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "flatten_request")
claude_sonu = next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt")
handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H")
responses = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_responses")
ozgun = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_ozgun")
taraf = [n for n in source.body if isinstance(n, ast.FunctionDef) and n.name in ("ek_model_mu", "yonlendirilen")]
class AccessPrograms(unittest.TestCase):
def setUp(self):
self.upstream = Mock(return_value=Mock(status=200))
self.env = {
"json": json,
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json,
"urllib": Mock(request=Mock(Request=urllib.request.Request, urlopen=self.upstream)),
"decompress": lambda body, encoding: body,
"MODEL_MAP": {},
@@ -31,10 +33,10 @@ class AccessPrograms(unittest.TestCase):
"fix_tool_ids": lambda req: 0,
"drop_foreign_items": lambda req: 0,
"SseRewriter": Mock(),
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"log": lambda message: None, "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "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)
exec(compile(ast.Module(body=taraf + [flatten, claude_sonu, responses, ozgun], type_ignores=[]), "bridge.py", "exec"), self.env)
def request(self, model):
return {"model": model, "access_programs": {"cyber": "standard"},
@@ -74,8 +76,10 @@ class AccessPrograms(unittest.TestCase):
def test_chatgpt_passthrough_preserves_original_body(self):
body = json.dumps(self.request("gpt-6-astra")).encode()
fake = Mock(headers={})
fake._ozgun = lambda *a: self.env["_ozgun"](fake, *a)
self.env["_responses"](fake, 1, "/responses", body)
self.assertEqual(fake._passthrough.call_args.args[2], body)
self.assertFalse(fake._passthrough.call_args.kwargs["rewritten"])
self.upstream.assert_not_called()
@@ -0,0 +1,224 @@
"""The splitter (bridge.py): Codex's own models go to chatgpt.com byte for byte, added models to the bridge process.
Real sockets, no network: a fake chatgpt.com and a fake bridge process record what reaches them."""
import gzip
import hashlib
import http.client
import http.server
import json
import tempfile
import threading
import time
import unittest
import bridge
class Kayit(http.server.BaseHTTPRequestHandler):
"""Records every request; answers from the server's `cevap` function."""
protocol_version = "HTTP/1.1"
def log_message(self, *a):
pass
def _her(self):
n = int(self.headers.get("Content-Length") or 0)
govde = self.rfile.read(n) if n else b""
self.server.gelen.append({"yontem": self.command, "yol": self.path, "baslik": dict(self.headers), "govde": govde})
self.server.cevap(self, govde)
do_GET = do_POST = _her
def sunucu(cevap):
s = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Kayit)
s.daemon_threads = True
s.gelen, s.cevap = [], cevap
threading.Thread(target=s.serve_forever, daemon=True).start()
return s
def duz(durum, govde, tur="application/json"):
def f(h, _):
h.send_response(durum)
h.send_header("Content-Type", tur)
h.send_header("Content-Length", str(len(govde)))
h.end_headers()
h.wfile.write(govde)
return f
def zstd(veri):
try:
from compression import zstd as z
return z.compress(veri)
except ImportError:
import zstandard
return zstandard.ZstdCompressor().compress(veri)
class Ayirici(unittest.TestCase):
def setUp(self):
self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream"))
def worker(h, body):
if h.path.endswith("/prepare-transfer"):
return duz(200, json.dumps({"request": json.loads(body)["request"]}).encode())(h, body)
return duz(200, b'event: response.completed\ndata: {"kopru":1}\n\n', "text/event-stream")(h, body)
self.kopru = sunucu(worker)
self.on = bridge.Sunucu(("127.0.0.1", 0), bridge.H)
threading.Thread(target=self.on.serve_forever, daemon=True).start()
self.eski = (bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, dict(bridge.CFG))
self.gecici = tempfile.TemporaryDirectory()
self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS)
bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json"
bridge.TRANSFERS.clear()
bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}"
bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}"
bridge.LOGDIR = self.gecici.name
bridge.CFG.clear()
def tearDown(self):
for s in (self.on, self.ozgun, self.kopru):
s.shutdown()
s.server_close()
bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, cfg = self.eski
bridge.CFG.clear()
bridge.CFG.update(cfg)
bridge.TRANSFER_PATH, transfers = self.transfer_old
bridge.TRANSFERS.clear()
bridge.TRANSFERS.update(transfers)
self.gecici.cleanup()
def iste(self, yontem, yol, govde=None, baslik=None):
b = http.client.HTTPConnection("127.0.0.1", self.on.server_address[1], timeout=10)
b.request(yontem, yol, body=govde, headers=baslik or {})
y = b.getresponse()
veri = y.read()
b.close()
return y.status, veri, dict(y.getheaders())
def test_codex_own_model_reaches_chatgpt_byte_for_byte(self):
"""Compressed or not, with a command, a lone dot or a compaction trigger in it: nothing is changed."""
istekler = [{"model": "gpt-6-astra", "input": [{"role": "user", "content": "modeli opus 5 5 yap"}]},
{"model": "gpt-6-luna", "input": [{"role": "user", "content": "."}]},
{"model": "gpt-6-astra", "prompt_cache_key": "s", "input": [{"type": "compaction_trigger"}]},
{"model": "gpt-6-astra", "input": [{"type": "compaction", "encrypted_content": "gAAAA_native"},
{"type": "reasoning", "id": "rs_native"}]}]
for veri in istekler:
ham = json.dumps(veri, ensure_ascii=False).encode("utf-8")
for kodlama, govde in (("zstd", zstd(ham)), ("gzip", gzip.compress(ham)), ("", ham)):
with self.subTest(model=veri["model"], kodlama=kodlama):
baslik = {"Authorization": "Bearer sahte", "originator": "Codex Desktop", "chatgpt-account-id": "h1"}
if kodlama:
baslik["Content-Encoding"] = kodlama
durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", govde, baslik)
giden = self.ozgun.gelen[-1]
self.assertEqual(hashlib.sha256(giden["govde"]).hexdigest(), hashlib.sha256(govde).hexdigest())
basliklar = {k.lower(): v for k, v in giden["baslik"].items()}
self.assertEqual(basliklar.get("content-encoding", ""), kodlama)
self.assertEqual((basliklar["authorization"], basliklar["originator"], basliklar["chatgpt-account-id"]),
("Bearer sahte", "Codex Desktop", "h1"))
self.assertEqual(giden["yol"], "/backend-api/codex/responses")
self.assertEqual((durum, cevap), (200, b'event: response.completed\ndata: {"ok":1}\n\n'))
self.assertEqual(self.kopru.gelen, []) # the bridge process never saw any of them
def test_added_models_go_to_the_bridge_process_and_only_there(self):
for model in ("ide/Opus-5", "9r/cc/claude-opus-5-5"):
govde = zstd(json.dumps({"model": model, "input": []}).encode())
durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", govde,
{"Content-Encoding": "zstd", "originator": "Codex Desktop"})
self.assertEqual((durum, cevap), (200, b'event: response.completed\ndata: {"kopru":1}\n\n'))
self.assertEqual(self.kopru.gelen[-1]["govde"], govde)
basliklar = {k.lower(): v for k, v in self.kopru.gelen[-1]["baslik"].items()}
self.assertEqual((basliklar["originator"], basliklar["content-encoding"]), ("Codex Desktop", "zstd"))
bridge.CFG["model_map"] = {"ozel-model": ["rota", "hedef"]}
self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ozel-model"}).encode())
self.assertEqual(len([x for x in self.kopru.gelen if x["yol"].endswith("/responses")]), 3)
self.assertEqual(self.ozgun.gelen, [])
def test_a_body_that_cannot_be_read_takes_codex_own_path_untouched(self):
for govde, baslik in ((b"\x00\x01bozuk", {"Content-Encoding": "zstd"}), (b"json degil", {}), (b"[1,2]", {})):
self.iste("POST", "/backend-api/codex/responses", govde, baslik)
self.assertEqual(self.ozgun.gelen[-1]["govde"], govde)
self.assertEqual(self.kopru.gelen, [])
def test_codex_own_model_keeps_working_while_the_bridge_process_is_down(self):
self.kopru.shutdown()
self.kopru.server_close()
govde = json.dumps({"model": "gpt-6-astra", "input": []}).encode()
self.assertEqual(self.iste("POST", "/backend-api/codex/responses", govde)[0], 200)
# The model list falls back to chatgpt.com's own list, unchanged.
self.ozgun.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"}]}')
durum, liste, _ = self.iste("GET", "/backend-api/codex/models?client_version=1", None, {"If-None-Match": "x"})
self.assertEqual((durum, liste), (200, b'{"models":[{"slug":"gpt-6-astra"}]}'))
# An added model says so instead of hanging or going to chatgpt.com under another name.
durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ide/Opus-5"}).encode())
self.assertEqual(durum, 200)
self.assertIn(b"response.failed", cevap)
self.assertEqual([g["yol"].split("?")[0] for g in self.ozgun.gelen],
["/backend-api/codex/responses", "/backend-api/codex/models"])
# Status still answers in the shape AITURK IDE expects.
durum, cevap, _ = self.iste("GET", "/aiturk/codex-baglama/status")
d = json.loads(cevap)
self.assertEqual((durum, d["surum"], d["kopru"], d["ayirici"]), (200, bridge.SURUM, "kapali", True))
def test_model_list_comes_from_the_bridge_and_falls_back_when_it_fails(self):
self.kopru.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"},{"slug":"ide/Opus-5"}]}')
self.assertIn(b"ide/Opus-5", self.iste("GET", "/backend-api/codex/models")[1])
self.assertEqual(self.ozgun.gelen, [])
self.kopru.cevap = duz(500, b'{"error":"x"}')
self.ozgun.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"}]}')
self.assertEqual(self.iste("GET", "/backend-api/codex/models")[1], b'{"models":[{"slug":"gpt-6-astra"}]}')
def test_everything_else_and_errors_pass_through_as_they_are(self):
self.ozgun.cevap = duz(200, b'{"sonuc":1}')
self.assertEqual(self.iste("POST", "/backend-api/codex/alpha/search", b'{"q":"x"}')[1], b'{"sonuc":1}')
self.assertEqual(self.ozgun.gelen[-1]["govde"], b'{"q":"x"}')
self.ozgun.cevap = duz(400, b'{"error":{"message":"gecersiz"}}')
durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "gpt-6-astra"}).encode())
self.assertEqual((durum, cevap), (400, b'{"error":{"message":"gecersiz"}}'))
self.assertEqual(self.kopru.gelen, [])
def test_a_long_answer_streams_piece_by_piece_without_waiting_for_the_end(self):
"""This morning's failure was an idle stream: Codex got no bytes for five minutes. Bytes must flow as they come."""
def yavas(h, _):
h.send_response(200)
h.send_header("Content-Type", "text/event-stream")
h.send_header("Transfer-Encoding", "chunked")
h.end_headers()
for parca in (b"data: bir\n\n", b"data: iki\n\n"):
h.wfile.write(b"%x\r\n" % len(parca) + parca + b"\r\n")
h.wfile.flush()
time.sleep(1.0)
h.wfile.write(b"0\r\n\r\n")
self.ozgun.cevap = yavas
b = http.client.HTTPConnection("127.0.0.1", self.on.server_address[1], timeout=10)
bas = time.time()
b.request("POST", "/backend-api/codex/responses", body=json.dumps({"model": "gpt-6-astra"}).encode())
y = b.getresponse()
ilk = y.read1(64)
ilk_sure = time.time() - bas
kalan = y.read()
b.close()
self.assertEqual(ilk, b"data: bir\n\n")
self.assertLess(ilk_sure, 0.8) # the first piece arrived before the answer ended
self.assertEqual(kalan, b"data: iki\n\n")
def test_status_is_the_bridge_status_plus_the_splitter_fields(self):
self.kopru.cevap = duz(200, b'{"ok": true, "surum": "8", "kod": "abc", "ide_durum": "tamam", "ide_model": 147}')
d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1])
self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True))
def test_a_persistent_connection_is_refused_the_way_it_is_today(self):
durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"})
self.assertEqual(durum, 426)
self.assertEqual(self.ozgun.gelen + self.kopru.gelen, [])
def test_which_side_a_model_is_on(self):
for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False),
("", False), (None, False), ("idea", False)):
self.assertEqual(bridge.ek_model_mu(slug), ek, slug)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,357 @@
import concurrent.futures
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import subprocess
import sys
import threading
import time
import pytest
from bekci_watch import Frames, Ledger, MetadataFilter, WatchOwner, opaque
from native_speech_mux import NativeSpeechMux, stdio_invocation
from test_native_speech_mux import Harness
def wait(predicate):
until=time.time()+5
while time.time()<until:
if predicate(): return
time.sleep(.01)
assert predicate()
class Server:
def __init__(self, frames, status=200):
self.requests=[];self.frames=frames;self.status=status;self.closed=threading.Event()
owner=self
class H(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
owner.requests.append((self.path,self.headers.get('Authorization'),json.loads(self.rfile.read(int(self.headers['Content-Length'])))))
self.send_response(owner.status);self.send_header('Content-Type','text/event-stream');self.end_headers()
try:
for frame in owner.frames:
self.wfile.write(frame);self.wfile.flush()
while not owner.closed.wait(.05):
self.wfile.write(b': keepalive\n\n');self.wfile.flush()
except OSError: pass
self.server=ThreadingHTTPServer(('127.0.0.1',0),H);self.server.daemon_threads=True
self.origin='http://127.0.0.1:'+str(self.server.server_port)
threading.Thread(target=self.server.serve_forever,daemon=True).start()
def close(self): self.closed.set();self.server.shutdown();self.server.server_close()
def frame(event,value,cursor=None):
return ((f'id: {cursor}\n' if cursor else '')+f'event: {event}\ndata: '+json.dumps(value)+'\n\n').encode()
def started(mux,tid):
mux._observe_server({'method':'thread/realtime/started','params':{'threadId':tid,'realtimeSessionId':'rt'}})
def meta(): return dict(subscription='s'*40,events_url='/v1/bekci/events',expires_at=(time.time()+60)*1000)
def capture(owner,tid): return owner.capture({'session-id':tid,'thread-id':tid},{'prompt_cache_key':tid})
def notice(i='one'): return dict(notice=dict(id=i,type='arastirma',metin='correction',at=time.time()),cursor='cursor-'+i)
def test_stream_metadata_isolation_split_crlf_and_json():
seen=[];f=MetadataFilter(seen.append);wire=frame('response.output_text.delta',{'delta':'answer'})+frame('bekci.watch',meta())+b'data: [DONE]\n\n'
wire=wire.replace(b'\n',b'\r\n');out=b''
for byte in wire: out+=f.feed(bytes([byte]))
out+=f.flush()
assert len(seen)==1 and b'subscription' not in out and b'answer' in out and b'[DONE]' in out
def test_ready_auth_durable_replay_and_epoch_abort(tmp_path):
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor']),frame('notice',n,n['cursor'])])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
ledger=Ledger(tmp_path/'ledger.db');owner=WatchOwner(mux,ledger,origin=server.origin,test_loopback=True)
c=capture(owner,'A');p=meta()
try:
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='subscribing'
wait(lambda:mux._queue.qsize()==1)
scope=opaque('public-watch','member',p['subscription'])
wait(lambda:ledger.cursor(scope)==n['cursor'])
assert server.requests[0]==('/v1/bekci/events','Bearer member',{'subscription':p['subscription']})
reopened=Ledger(tmp_path/'ledger.db');assert not reopened.claim(scope,'one')
mux._observe_client({'method':'thread/realtime/stop','params':{'threadId':'A'}})
wait(lambda:not owner.watches)
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='stale_generation'
finally: owner.close();server.close()
def test_no_ready_wrong_key_untrusted_url_and_two_threads(tmp_path):
n=notice();server=Server([frame('notice',n,n['cursor'])]);mux=NativeSpeechMux(['unused'],observer=True)
for t in ('A','B'): started(mux,t)
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
c=capture(owner,'A')
assert owner.accept(meta(),c,'member','https://evil.invalid/responses')=='untrusted_route'
assert owner.accept(meta(),None,'member',server.origin)=='stale_generation'
p=meta();p['events_url']='https://evil.invalid';assert owner.accept(p,c,'member',server.origin)=='invalid_receipt'
owner.accept(meta(),c,'member',server.origin);wait(lambda:len(server.requests)>0)
time.sleep(.1);assert mux._queue.empty() and not mux._bindings
finally:owner.close();server.close()
server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])]);owner=WatchOwner(mux,Ledger(tmp_path/'db2'),origin=server.origin,test_loopback=True)
try:
for t in ('A','B'):
p=meta();p['subscription']=t*40;owner.accept(p,capture(owner,t),'member',server.origin)
wait(lambda:mux._queue.qsize()==2)
assert {mux._queue.get()[1][0]['thread_id'] for _ in range(2)}=={'A','B'}
finally:owner.close();server.close()
def test_launcher_stdio_grammar(tmp_path):
assert stdio_invocation(['-c','x=true','app-server','--analytics-default-enabled','-c','y="z"'])
assert not stdio_invocation(['exec','app-server'])
assert not stdio_invocation(['app-server','--listen','ws://127.0.0.1'])
assert not stdio_invocation(['app-server','daemon','start'])
@pytest.mark.parametrize('active_owner', [True, False])
def test_http_receipt_through_real_bridge_handler_to_real_pipe(tmp_path, active_owner):
"""Real imported H, real HTTP primary/events, real subprocess append/approval."""
import os
import http.client
import urllib.request
bridge=tmp_path/'bridge';bridge.mkdir()
(bridge/'bridge.json').write_text(json.dumps({'aiturk_hat':{'enabled':False},'sesle_model':False,'nokta_karsila':False,'yarim_cevap':False,'ozet_kopru':{'enabled':False},'tasma_bekcisi':False}),encoding='utf-8')
agent=tmp_path/'agent';agent.mkdir()
old=dict(os.environ);os.environ['AITURK_CODEX_BRIDGE_HOME']=str(bridge);os.environ['HERMES_HOME']=str(agent)
try:
import kopru
finally: os.environ.clear();os.environ.update(old)
h=Harness(tmp_path);h.start()
p=meta();n=notice();requests=[]
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
requests.append((self.path,self.headers.get('Authorization'),body))
if self.path=='/v1/bekci/events':
wire=frame('ready',{'cursor':'start'})+frame('notice',n,n['cursor'])
kind='text/event-stream'
elif body.get('stream',True):
wire=frame('response.output_text.delta',{'type':'response.output_text.delta','delta':'answer'})+frame('bekci.watch',p)+b'data: [DONE]\n\n';kind='text/event-stream'
else:
wire=json.dumps({'output':[],'_bekci_watch':p}).encode();kind='application/json'
self.send_response(200);self.send_header('Content-Type',kind);self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary);upstream.daemon_threads=True
threading.Thread(target=upstream.serve_forever,daemon=True).start();origin='http://127.0.0.1:'+str(upstream.server_port)
owner=WatchOwner(h.mux,Ledger(tmp_path/'http-ledger'),origin=origin,test_loopback=True)
kopru.WATCH_OWNER=owner if active_owner else None;kopru.MODEL_MAP['ide/test-watch']=('test-route','Sesli-Hizli-Zeki',False)
kopru.ROUTES['test-route']={'url':origin+'/v1/responses','key':'public-member','flatten_tools':False}
server=kopru.TS(('127.0.0.1',0),kopru.H);threading.Thread(target=server.serve_forever,daemon=True).start()
try:
for stream in (True,False):
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/test-watch','stream':stream,'prompt_cache_key':'thread-A','input':[]}),{'Content-Type':'application/json','session-id':'thread-A','thread-id':'thread-A'})
resp=conn.getresponse();data=resp.read();conn.close()
assert resp.status==200 and b'subscription' not in data and b'_bekci_watch' not in data
if active_owner:
approval=json.loads(h.recv());speech=json.loads(h.recv())
assert approval['method']=='item/commandExecution/requestApproval'
assert speech['method']=='seen-speech' and speech['params']=={'threadId':'thread-A','text':'correction'}
assert any(path=='/v1/bekci/events' and auth=='Bearer public-member' for path,auth,_ in requests)
else:
assert not any(path=='/v1/bekci/events' for path,_,_ in requests)
finally:
owner.close();server.shutdown();server.server_close();upstream.shutdown();upstream.server_close();kopru.WATCH_OWNER=None;h.close()
def test_http_403_expiry_and_crash_claim_never_resend(tmp_path):
server=Server([],status=403);mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
owner.accept(meta(),capture(owner,'A'),'wrong-key',server.origin)
wait(lambda:len(server.requests)==1);wait(lambda:not owner.watches)
assert not mux._bindings and mux._queue.empty()
finally:owner.close();server.close()
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])])
ledger=Ledger(tmp_path/'crash');p=meta();scope=opaque('public-watch','member',p['subscription']);assert ledger.claim(scope,'one')
owner=WatchOwner(mux,Ledger(tmp_path/'crash'),origin=server.origin,test_loopback=True)
try:
p['expires_at']=(time.time()+.4)*1000
owner.accept(p,capture(owner,'A'),'member',server.origin)
wait(lambda:owner.ledger.cursor(scope)==n['cursor']);wait(lambda:not owner.watches)
assert mux._queue.empty()
finally:owner.close();server.close()
def test_receipt_cannot_move_to_other_thread_or_generation(tmp_path):
server=Server([frame('ready',{'cursor':'start'})]);mux=NativeSpeechMux(['unused'],observer=True)
started(mux,'A');started(mux,'B');owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True);p=meta()
try:
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
assert owner.accept(p,capture(owner,'B'),'member',server.origin)=='receipt_replay_mismatch'
mux._observe_client({'method':'thread/unsubscribe','params':{'threadId':'A'}})
wait(lambda:not owner.watches);started(mux,'A')
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='receipt_replay_mismatch'
finally:owner.close();server.close()
@pytest.mark.parametrize('seconds', [16*60, 60*60, 24*60*60, 24*60*60+30])
def test_protocol_lifetime_receipt_admitted(tmp_path, seconds):
server=Server([frame('ready', {'cursor':'start'})])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
p=meta();p['expires_at']=(time.time()+seconds)*1000
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
wait(lambda:len(server.requests)==1)
wait(lambda:bool(mux._bindings))
assert owner.watches
finally: owner.close();server.close()
@pytest.mark.parametrize('expiry', [0, -1, float('inf'), float('-inf'), float('nan'), True, False, 'infinite', None, 10**100])
def test_invalid_or_unbounded_expiry_rejected(tmp_path, expiry):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=expiry
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
assert not owner.watches
def test_receipt_beyond_protocol_and_skew_rejected(tmp_path):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=(time.time()+24*60*60+120)*1000
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
@pytest.mark.parametrize('target', ['Other-Model', 'sesli-hizli-zeki', 'Sesli-Hizli-Zeki-extra'])
@pytest.mark.parametrize('json_response', [False, True])
@pytest.mark.parametrize('jev', [False, True])
def test_actual_handler_nonvoice_preserves_response_pipeline(tmp_path, monkeypatch, target, json_response, jev):
import http.client
import kopru
from unittest.mock import Mock
# Exceed the sideband frame cap: other models must never enter its buffer.
wire=(json.dumps({'output':[], 'payload':'x'*270000}).encode() if json_response
else frame('response.output_text.delta', {'delta':'x'*270000}))
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
self.rfile.read(int(self.headers['Content-Length']))
self.send_response(200)
self.send_header('Content-Type','application/json' if json_response else 'text/event-stream')
self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary)
threading.Thread(target=upstream.serve_forever,daemon=True).start()
owner=Mock();monkeypatch.setattr(kopru,'WATCH_OWNER',owner)
monkeypatch.setitem(kopru.MODEL_MAP,'ide/scope-proof',('aiturk-ide',target,None))
monkeypatch.setitem(kopru.ROUTES,'aiturk-ide',{'url':f'http://127.0.0.1:{upstream.server_port}/v1/responses','key':'scope-member','flatten_tools':jev})
monkeypatch.setattr(kopru.jev_denetim,'anahtar',lambda:'test-key' if jev else '')
wrapped=Mock(side_effect=AssertionError('nonvoice entered watch wrapper'))
monkeypatch.setattr(kopru,'WatchResponse',wrapped)
shortcut=Mock(side_effect=AssertionError('nonvoice entered JSON shortcut'))
monkeypatch.setattr(kopru.H,'_send_json',shortcut)
calls=[]
original_stream=kopru.H._stream
def stream(self,resp,*args,**kwargs):
calls.append('stream')
return original_stream(self,resp,*args,**kwargs)
def denetim(self,resp,tag,rewriter,izle,istek,sira,ac,*,hedef):
# Verify original upstream object/body reaches Jev, not a watch wrapper.
calls.append('jev');assert hedef==target and sira
data=resp.read();resp.close();assert data==wire
self.send_response(200);self.send_header('Content-Length',str(len(data)))
self.end_headers();self.wfile.write(data)
monkeypatch.setattr(kopru.H,'_stream',stream)
monkeypatch.setattr(kopru.H,'_denetimli_akis',denetim)
server=kopru.TS(('127.0.0.1',0),kopru.H)
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/scope-proof','stream':not json_response,'input':[]}),{'Content-Type':'application/json'})
response=conn.getresponse();data=response.read();conn.close()
assert response.status==200 and data==wire
assert calls==(['jev'] if jev else ['stream'])
wrapped.assert_not_called();shortcut.assert_not_called();owner.accept.assert_not_called()
finally:
server.shutdown();server.server_close();upstream.shutdown();upstream.server_close()
class QuietWatchServer:
"""Real sockets, with no heartbeat; observes client shutdown at the peer."""
def __init__(self, protocol='HTTP/1.0', delay=0, headers_delay=False, reconnects=False, delayed_headers=False):
self.requests=[];self.closed=threading.Event();self.peer_closed=threading.Event()
self.ready=threading.Event();owner=self
class H(BaseHTTPRequestHandler):
protocol_version='HTTP/1.1' if protocol=='HTTP/1.1-close' else protocol
def log_message(self,*args): pass
def do_POST(self):
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
owner.requests.append(body);number=len(owner.requests)
try:
if headers_delay:
owner.ready.set()
self.connection.settimeout(5)
if self.connection.recv(1)==b'': owner.peer_closed.set()
return
if delayed_headers and owner.closed.wait(delay): return
self.send_response(200);self.send_header('Content-Type','text/event-stream')
if protocol=='HTTP/1.1-close': self.send_header('Connection','close')
self.end_headers()
self.wfile.write(frame('ready',{'cursor':'start'}));self.wfile.flush();owner.ready.set()
if reconnects:
n=notice('first' if number<9 else 'ninth')
self.wfile.write(frame('notice',n,n['cursor']));self.wfile.flush()
if number<9:
self.close_connection=True
return
elif delay and number==1:
if not delayed_headers and owner.closed.wait(delay): return
n=notice('delayed');self.wfile.write(frame('notice',n,n['cursor']));self.wfile.flush()
self.connection.settimeout(5 if not delay else 20)
if self.connection.recv(1)==b'': owner.peer_closed.set()
except OSError: pass
self.server=ThreadingHTTPServer(('127.0.0.1',0),H);self.server.daemon_threads=True
self.origin='http://127.0.0.1:'+str(self.server.server_port)
threading.Thread(target=self.server.serve_forever,daemon=True).start()
def close(self):
self.closed.set();self.server.shutdown();self.server.server_close()
@pytest.mark.parametrize('delayed_headers',[False,True])
def test_quiet_watch_delivers_after_more_than_ten_seconds(tmp_path,delayed_headers):
server=QuietWatchServer(delay=11.2,delayed_headers=delayed_headers)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'quiet'),origin=server.origin,test_loopback=True)
try:
assert owner.accept(meta(),capture(owner,'A'),'member',server.origin)=='subscribing'
assert server.ready.wait(14)
until=time.monotonic()+14
while mux._queue.empty() and time.monotonic()<until: time.sleep(.02)
assert not mux._queue.empty(), 'valid quiet stream lost delayed notice'
assert len(server.requests)==1, 'quiet channel spuriously reconnected'
finally:owner.close();server.close()
@pytest.mark.parametrize('protocol',['HTTP/1.0','HTTP/1.1','HTTP/1.1-close'])
@pytest.mark.parametrize('cancel',['close','expiry','generation'])
@pytest.mark.parametrize('headers_delay',[False,True])
def test_quiet_owned_socket_is_interrupted(tmp_path,protocol,cancel,headers_delay):
server=QuietWatchServer(protocol=protocol,headers_delay=headers_delay)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'cancel'),origin=server.origin,test_loopback=True)
try:
p=meta()
if cancel=='expiry':p['expires_at']=(time.time()+.7)*1000
owner.accept(p,capture(owner,'A'),'member',server.origin)
assert server.ready.wait(3)
if not headers_delay:wait(lambda:bool(mux._bindings))
if cancel=='close':owner.close()
elif cancel=='generation':mux._observe_client({'method':'thread/realtime/stop','params':{'threadId':'A'}})
assert server.peer_closed.wait(2), 'owned response/header socket remained open'
wait(lambda:not owner.watches)
assert not mux._bindings and mux._queue.empty()
finally:owner.close();server.close()
def test_ninth_reconnect_delivers_and_preserves_cursor_dedup(tmp_path):
class FastBackoff:
def __init__(self):self.event=threading.Event()
def is_set(self):return self.event.is_set()
def set(self):self.event.set()
def wait(self,seconds):return self.event.wait(min(seconds,.01))
server=QuietWatchServer(reconnects=True)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'reconnect'),origin=server.origin,test_loopback=True)
p=meta();c=capture(owner,'A');scope=opaque('public-watch','member',p['subscription']);stop=FastBackoff()
owner.watches[scope]=stop
worker=threading.Thread(target=owner._watch,args=(p,c,'member',scope,stop),daemon=True);worker.start()
try:
wait(lambda:len(server.requests)>=9)
wait(lambda:owner.ledger.cursor(scope)=='cursor-ninth')
assert mux._queue.qsize()==2
assert all(req.get('cursor')=='cursor-first' for req in server.requests[1:9])
finally:
owner.close();worker.join(timeout=3);server.close()
assert not worker.is_alive()
@@ -4,7 +4,7 @@ import json
from pathlib import Path
import unittest
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
env = {}
exec(compile(ast.Module(body=[n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == "claude_sonu_duzelt"], type_ignores=[]), "bridge.py", "exec"), env)
f = env["claude_sonu_duzelt"]
@@ -13,7 +13,7 @@ from unittest.mock import Mock
import urllib.error
import urllib.request
source = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
source = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
pick = lambda name: next(n for n in source.body if isinstance(n, ast.FunctionDef) and n.name == name)
handler = next(n for n in source.body if isinstance(n, ast.ClassDef) and n.name == "H")
responses = next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == "_responses")
@@ -27,7 +27,7 @@ class ComboNotResent(unittest.TestCase):
def setUp(self):
self.upstream = Mock()
self.env = {
"json": json, "re": __import__("re"), "os": __import__("os"),
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "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,10 @@ 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": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "nokta_mesaji_mi": lambda req: False, "EXTRA_MODELS": [],
"NO_RETRY_PATTERNS": [], "pencere": __import__("pencere"), "IDE_BILGI": {}, "yarim": __import__("yarim"), "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "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("ek_model_mu"), pick("yonlendirilen"), 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 +86,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,519 @@
"""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, patch
import ide_catalog
import is_hatti
import model_gecis
import ozet_kopru
import pencere
import resim_context
import yarim
SOURCE = Path(__file__).with_name("kopru.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"),
{"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "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", "ek_model_mu", "yonlendirilen")
+ [method("_responses"), method("_model_komutu"), method("_ozgun")], {
"gecit_modelleri": lambda: [],
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "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, "jev_denetim": __import__("jev_denetim"), "internet": __import__("internet"), "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"]
Sahte._ozgun = self.env["_ozgun"]
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_a_request_for_codex_own_model_is_passed_through_untouched(self):
"""User decision, 11 Oct 2026: Codex's own models are one side, the added models the other; the bridge
does not touch a request for Codex's own model (no command, dot, note, work line or compaction handling)."""
self.env["CFG"].update({"nokta_karsila": True, "model_bilgi": True, "yarim_cevap": True,
"ozet_kopru": {"enabled": True, "kapsam": "hepsi"},
"aiturk_hat": {"enabled": True, "komut": "aiturk is"}})
mid_turn = [user("işe başla"), CALL, OUT, user("modeli opus 5 5 yap"), developer("k1")]
for items in ([user("modeli opus 5 5 yap")], [user(".")], [user("seçili model hangisi")], mid_turn,
[user("uzun iş"), {"type": "compaction_trigger"}]):
with self.subTest(items=items):
fake = Sahte(headers={"originator": "Codex Desktop"})
body = json.dumps({"model": "gpt-6-astra", "prompt_cache_key": "sohbet-1", "input": items}).encode()
fake._responses(1, "/responses", body)
fake._yerel_yanit.assert_not_called()
args, kwargs = fake._passthrough.call_args
self.assertEqual(args[2], body)
self.assertFalse(kwargs["rewritten"])
self.assertEqual(self.env["YONLENDIRME"].veri, {})
def test_the_bridge_does_not_even_help_codex_own_model(self):
"""'If you pick Codex and write to Codex, our bridge cannot be beside it even to help.' Traces an added
model or the bridge left earlier in the chat are not cleaned either: the body goes out byte for byte."""
self.env["CFG"]["ozet_kopru"] = {"enabled": True, "kapsam": "hepsi"}
self.env["fix_tool_ids"] = Mock(return_value=2)
self.env["drop_foreign_items"] = Mock(return_value=3)
summary = ozet_kopru.ozet_ogesi("eski köprü özeti", "t")
fake, body = self.send("gpt-6-astra", [summary, {"type": "reasoning", "id": "rs_chatcmpl-x"}, user("devam")])
args, kwargs = fake._passthrough.call_args
self.assertEqual(args[2], body)
self.assertFalse(kwargs["rewritten"])
self.env["fix_tool_ids"].assert_not_called()
self.env["drop_foreign_items"].assert_not_called()
fake._yerel_yanit.assert_not_called()
def test_voice_switch_stays_among_the_added_models(self):
opus, flash = "ide/9router-cc-claude-opus-5-5", "ide/9router-ag-gemini-3.8-flash-low"
self.env["ROUTES"]["aiturk-ide"] = {"url": "https://example.invalid/v1/responses", "key": "k"}
fake = Sahte()
answer = fake._model_komutu(1, {"input": [user("modeli gemini flash low yap")]}, "sohbet-1", opus)
self.assertIn("Sohbetin modeli artık agy/gemini-3.8-flash-low", answer)
self.assertEqual(self.env["yonlendirilen"]("sohbet-1", opus), flash)
# Codex's own model is not a voice target, from an added model either.
answer = fake._model_komutu(1, {"input": [user("modeli gpt 6 astra yap")]}, "sohbet-1", opus)
self.assertIn("GPT-6-Astra Codex'in kendi modeli", answer)
self.assertIn("sesle geçilmiyor", answer)
self.assertIn('"."', answer)
self.assertEqual(self.env["yonlendirilen"]("sohbet-1", opus), flash)
# In the middle of a turn the running model is told to pass that on; the work is not cut.
req = {"input": [user("işe başla"), CALL, OUT, user("modeli gpt 6 astra yap")]}
self.assertIsNone(fake._model_komutu(1, req, "sohbet-1", opus))
self.assertIn("sesle yapılamıyor", req["input"][3]["content"][0]["text"])
# A record left from before the rule that points at Codex's own model is ignored.
self.env["YONLENDIRME"].koy("sohbet-2", opus, "gpt-6-astra")
self.assertIsNone(self.env["yonlendirilen"]("sohbet-2", opus))
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_is_answered_for_an_added_model_and_left_alone_for_codex_own(self):
self.env["CFG"]["nokta_karsila"] = True
fake, _ = self.send("ide/9router-cc-claude-opus-5-5", [user(".")])
self.assertIn("cc/opus-5-5 seçildi", fake._yerel_yanit.call_args.args[1])
self.assertNotIn("hazır", fake._yerel_yanit.call_args.args[1])
fake, body = self.send("gpt-6-sol", [user(".")])
fake._yerel_yanit.assert_not_called()
self.assertEqual(fake._passthrough.call_args.args[2], body)
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_resolved_voice_target_and_codex_instructions_reach_the_stream_guard(self):
self.env["ROUTES"]["aiturk-ide"] = {
"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": True}
self.env["MODEL_MAP"]["ide/voice-alias"] = ("aiturk-ide", "Sesli-Hizli-Zeki", None)
req = {"model": "ide/voice-alias", "instructions": "Codex system instructions unchanged",
"input": [developer("Codex developer instructions unchanged"), user("selam")]}
for failure in (None, urllib.error.URLError(ConnectionResetError("reset")),
urllib.error.HTTPError("https://example.invalid", 400, "bad", {}, io.BytesIO(b"bad"))):
with self.subTest(failure=type(failure).__name__):
opener = self.env["urllib"].request.urlopen
opener.reset_mock()
opener.side_effect = failure
opener.return_value = Mock(status=200)
fake = Sahte()
fake._denetimli_akis = Mock()
with patch.object(self.env["jev_denetim"], "anahtar", return_value="test-only"):
fake._responses(1, "/responses", json.dumps(req).encode())
fake._denetimli_akis.assert_called_once()
self.assertEqual(fake._denetimli_akis.call_args.kwargs, {"hedef": "Sesli-Hizli-Zeki"})
self.assertEqual(fake._denetimli_akis.call_args.args[5], list(self.env["jev_denetim"].KATMAN))
sent = json.loads(opener.call_args.args[0].data)
self.assertEqual(sent["model"], "Sesli-Hizli-Zeki")
self.assertEqual(sent["instructions"], req["instructions"])
self.assertEqual(sent["input"][0], req["input"][0])
def test_nonvoice_json_still_reaches_original_jev_with_upstream_identity(self):
self.env["ROUTES"]["aiturk-ide"] = {
"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": True}
self.env["MODEL_MAP"]["ide/scope"] = ("aiturk-ide", "Other-Model", None)
response = Mock(status=200, headers={"Content-Type": "application/json"})
self.env["urllib"].request.urlopen.return_value = response
watch = self.env["WatchResponse"] = Mock(side_effect=AssertionError("watch scope escaped"))
fake = Sahte(); fake._denetimli_akis = Mock()
with patch.object(self.env["jev_denetim"], "anahtar", return_value="test-only"):
fake._responses(1, "/responses", json.dumps({"model":"ide/scope", "input":[user("task")]}).encode())
fake._denetimli_akis.assert_called_once()
self.assertIs(fake._denetimli_akis.call_args.args[0], response)
self.assertEqual(fake._denetimli_akis.call_args.kwargs, {"hedef":"Other-Model"})
response.read.assert_not_called()
watch.assert_not_called(); fake._send_json.assert_not_called(); fake._stream.assert_not_called()
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 = {"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
"decompress": lambda raw, enc: raw, "EXTRA_MODELS": [], "IDE_PREFIX": "ide/", "SLIM_CLONES": False,
"SLIM_FIELDS": [], "ide_catalog": ide_catalog, "LAST_MODELS": {}, "save_last_models": Mock(),
"RESP_HOP": {"content-length", "content-encoding"}, "REQ_HOP": {"host"}, "CHATGPT": "https://example.invalid",
"UPSTREAM_IDLE_TIMEOUT": 1,
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
error=urllib.error)}
env.update(extra)
return build([method("_models"), method("_passthrough"), method("_son_listeyi_ver")], env)
UPSTREAM = {"models": [{"slug": "gpt-6-sol", "display_name": "GPT-6-Sol", "priority": 1},
{"slug": "gpt-6-astra", "display_name": "GPT-6-Astra", "priority": 2}]}
def test_gzip_answer_is_still_cached_plain_and_originals_keep_name_and_place(self):
extras = [{"slug": "gpt-6-astra", "display_name": "Baska Ad"}, {"slug": "ide/a", "display_name": "Aaa", "kind": "combo"}]
env = self.env(EXTRA_MODELS=extras, CFG={"display_map": {"gpt-6-sol": "Yeni Ad"}})
fake = Sahte({"Accept-Encoding": "gzip", "chatgpt-account-id": "hesap"}, "GET")
resp = types.SimpleNamespace(read=lambda: json.dumps(self.UPSTREAM).encode(), headers={"ETag": "x"})
env["_models"](fake, resp, "[t]")
listed = json.loads(gzip.decompress(fake.wfile.getvalue()))["models"]
self.assertEqual([(m["slug"], m["display_name"]) for m in listed],
[("gpt-6-sol", "GPT-6-Sol"), ("gpt-6-astra", "GPT-6-Astra"), ("ide/a", "Aaa")])
cached = env["LAST_MODELS"]["hesap"]
self.assertEqual(json.loads(cached["body"])["models"], listed) # plain JSON, usable as a fallback
self.assertNotIn("Content-Encoding", dict(cached["headers"]))
self.assertIn(("Content-Encoding", "gzip"), fake.sent)
env["save_last_models"].assert_called_once()
def test_no_network_still_serves_the_last_list(self):
cached = {"headers": [("Content-Type", "application/json")], "body": b'{"models": []}', "at": "x"}
env = self.env(LAST_MODELS={"hesap": cached})
env["urllib"].request.urlopen.side_effect = urllib.error.URLError(OSError("no network"))
fake = Sahte({"chatgpt-account-id": "hesap"}, "GET")
fake._son_listeyi_ver = types.MethodType(env["_son_listeyi_ver"], fake)
env["_passthrough"](fake, 1, "/backend-api/codex/models?client_version=1", b"")
self.assertEqual((fake.status, fake.wfile.getvalue()), (200, b'{"models": []}'))
other = Sahte({"chatgpt-account-id": "baska"}, "GET")
other._son_listeyi_ver = types.MethodType(env["_son_listeyi_ver"], other)
env["_passthrough"](other, 1, "/backend-api/codex/models", b"")
self.assertEqual(other._send_json.call_args.args[0], 502)
class Kucukler(unittest.TestCase):
def test_log_line_has_the_date_and_no_token_tail_and_status_reports_the_code(self):
self.assertIn('time.strftime("%Y-%m-%d %H:%M:%S ") + msg', SOURCE)
self.assertNotIn("token=..", SOURCE)
self.assertNotIn("DISPLAY_MAP", SOURCE)
self.assertIn('"kod": KOD', SOURCE)
def test_responses_path_with_a_query_is_still_routed(self):
env = build([method("_handle")], {"lock": __import__("threading").Lock(), "seq": [0], "DEBUG": False,
"log": lambda m: None})
fake = Sahte({})
fake.path = "/backend-api/codex/responses?stream=true"
fake._read = lambda: b"{}"
fake._responses = Mock()
env["_handle"](fake)
fake._responses.assert_called_once()
fake._passthrough.assert_not_called()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,252 @@
"""Offline regressions for cross-provider context migration and native voice."""
import ast
import copy
import hashlib
import json
import threading
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import unittest
import urllib.request
import gecis_context as gc
import ozet_kopru
import bridge
import test_ayirici as fixtures
from test_ayirici import duz, zstd, sunucu
from test_model_switch import env
class History(unittest.TestCase):
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
{"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "call_1", "input": "abc"},
{"type": "custom_tool_call_output", "id": "fc_out", "call_id": "call_1", "output": "result"},
{"type": "compaction", "encrypted_content": "gAAAA_native"}]}
before = copy.deepcopy(req)
result = gc.normalize(req)
self.assertEqual(req, before)
self.assertIn("KARAR-4827", result["input"][0]["content"][0]["text"])
self.assertEqual(result["input"][1]["id"], "ctc_call_173683")
self.assertEqual(result["input"][2]["id"], "ctco_out")
self.assertEqual(result["input"][3], req["input"][3])
self.assertEqual(result["input"][1]["call_id"], result["input"][2]["call_id"])
def test_already_custom_stream_ids_and_deltas(self):
adapter = env["SseRewriter"]({"apply_patch"})
item = {"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "call_1", "name": "apply_patch", "input": "abc"}
for event in ("response.output_item.added", "response.output_item.done"):
self.assertEqual(adapter._fix({"type": event, "item": dict(item)})["item"]["id"], "ctc_call_173683")
self.assertEqual(adapter._fix({"type": "response.custom_tool_call_input.delta", "item_id": item["id"], "delta": "a"})["item_id"], "ctc_call_173683")
final = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})
self.assertEqual(final["response"]["output"][0]["id"], "ctc_call_173683")
def test_big_history_summarizes_middle_instead_of_cutting_it(self):
req = {"model": "ide/test", "instructions": "keep instructions", "tools": [], "input": [
{"role": "user", "content": "A" * 60000 + "KARAR-4827" + "B" * 60000},
{"role": "assistant", "content": "work complete"},
{"role": "user", "content": "Son karari uygula"}]}
before = copy.deepcopy(req)
calls = []
def summarize(text, budget):
self.assertIn("KARAR-4827", text)
calls.append(budget)
return "KARAR-4827: Korunacak karar ve acik is. " * 5
prep = gc.Preparer()
result = prep.prepare(req, 32768, summarize, "account/thread")
self.assertEqual(req, before)
self.assertLessEqual(gc.tokens(result), int(32768 * .7))
self.assertEqual(result["instructions"], req["instructions"])
self.assertEqual(result["input"][-1], req["input"][-1])
self.assertIn("KARAR-4827", json.dumps(result))
self.assertEqual(prep.prepare(req, 32768, summarize, "account/thread"), result)
self.assertEqual(len(calls), 1)
def test_threshold_includes_tools_and_instructions(self):
req = {"instructions": "x" * 100000, "tools": [{"type": "function", "name": "p", "description": "y" * 30000}],
"input": [{"role": "user", "content": "hello"}]}
with self.assertRaises(gc.TransferError):
gc.Preparer().prepare(req, 32768, lambda *_: self.fail("must not summarize protected instructions"))
def test_failed_summary_never_replaces_history(self):
req = {"input": [{"role": "user", "content": "a" * 90000}, {"role": "user", "content": "latest"}]}
before = copy.deepcopy(req)
for response in ("", "too short", "x" * 100000):
with self.assertRaises(gc.TransferError):
gc.Preparer().prepare(req, 32768, lambda *_: response)
self.assertEqual(req, before)
def test_recent_tool_pairs_stay_together(self):
call = {"type": "custom_tool_call", "call_id": "p", "input": "abc"}
output = {"type": "custom_tool_call_output", "call_id": "p", "output": "result"}
protected, old, recent = gc.split_history([
{"role": "user", "content": "old"}, call, {"role": "user", "content": "latest"}, output], keep_users=1)
self.assertIn(call, recent)
self.assertIn(output, recent)
class PreparationService(unittest.TestCase):
def setUp(self):
# Extract just the real preparation methods: importing kopru starts its
# catalog and background threads, which must not happen in offline tests.
tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
owner = next(x for x in tree.body if isinstance(x, ast.ClassDef) and any(isinstance(m, ast.FunctionDef) and m.name == "_prepare_transfer" for m in x.body))
methods = [x for x in owner.body if isinstance(x, ast.FunctionDef) and x.name in
{"_transfer_window", "_transfer_summary", "_prepare_transfer"}]
self.ns = {"json": json, "hashlib": hashlib, "urllib": __import__("urllib"), "gecis_context": gc,
"ozet_kopru": ozet_kopru, "pencere": __import__("pencere"), "CFG": {}, "UA": "test",
"UPSTREAM_IDLE_TIMEOUT": 5, "MODEL_MAP": {"ide/test": ("aiturk", "test", True)},
"IDE_BILGI": {"ide/test": {"pencere": 32768, "kaynak": "yerel"}},
"LAST_MODELS": {"acct": {"body": json.dumps({"models": [{"slug": "gpt-6-astra", "context_window": 128000}]})}},
"ek_model_mu": lambda slug: slug.startswith("ide/"), "yonlendirilen": lambda *_: None}
def upstream(h, body):
data = json.loads(body)
self.assertEqual(data["model"], "test")
self.assertEqual(data["tools"], [])
self.assertEqual(h.headers["Authorization"], "Bearer gateway-test")
self.assertNotIn("chatgpt-account-id", h.headers)
text = "KARAR-4827: Dosya yolu ve acik is korunacak. " * 3
events = [{"type": "response.output_item.done", "item": {"type": "message", "content": [{"type": "output_text", "text": text}]}},
{"type": "response.completed", "response": {"status": "completed", "output": []}}]
raw = b"".join(b"data: " + json.dumps(e).encode() + b"\n\n" for e in events)
duz(200, raw, "text/event-stream")(h, body)
self.server = sunucu(upstream)
self.ns["ROUTES"] = {"aiturk": {"url": "http://127.0.0.1:%d/responses" % self.server.server_address[1], "key": "gateway-test"}}
cls = ast.ClassDef(name="Service", bases=[], keywords=[], body=methods, decorator_list=[])
exec(compile(ast.fix_missing_locations(ast.Module(body=[cls], type_ignores=[])), "preparation", "exec"), self.ns)
self.service = self.ns["Service"]()
self.service.headers = {"Authorization": "Bearer native-test", "chatgpt-account-id": "acct"}
self.service._openai_ozet_coz = lambda *_: None
def tearDown(self):
self.server.shutdown()
self.server.server_close()
def test_large_external_preflight_uses_aiturk_only_and_fits(self):
req = {"model": "ide/test", "input": [{"role": "user", "content": "x" * 60000 + "KARAR-4827"},
{"role": "user", "content": "latest"}]}
result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), int(32768 * .7))
self.assertIn("KARAR-4827", json.dumps(result))
self.assertGreater(len(self.server.gelen), 1) # bounded chunks, not oversized summarizer request
def test_subscription_window_is_read_from_its_own_catalog(self):
self.assertEqual(self.service._transfer_window("gpt-6-astra"), 128000)
self.assertEqual(self.service._transfer_window("ide/unknown"), 32768)
def test_output_reserve_can_require_preparation_below_seventy_percent(self):
req = {"model": "ide/test", "max_output_tokens": 24000,
"input": [{"role": "user", "content": "x" * 20000}, {"role": "user", "content": "latest"}]}
self.assertLess(gc.tokens(req), int(32768 * .7))
result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), 32768 - 24000)
self.assertTrue(self.server.gelen)
def test_unreadable_native_compaction_never_drops_context(self):
req = {"model": "ide/test", "input": [{"type": "compaction", "encrypted_content": "gAAAA_unreadable"}]}
with self.assertRaises(gc.TransferError):
self.service._prepare_transfer({"request": req})
self.assertEqual(self.server.gelen, [])
def test_external_compaction_uses_external_summary_not_subscription(self):
req = {"model": "ide/test", "input": [{"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
result = self.service._prepare_transfer({"request": req})
self.assertIn("KARAR-4827", ozet_kopru.ozet_metni(result["compaction"]))
self.assertEqual(len(self.server.gelen), 1)
class SocketMigration(fixtures.Ayirici):
def test_fifty_simultaneous_chats_keep_their_provider_routes(self):
start = threading.Barrier(50)
def call(index):
external = index % 2 == 0
req = {"model": "ide/test" if external else "gpt-6-astra",
"prompt_cache_key": f"parallel-{index}",
"input": [{"role": "user", "content": f"marker-{index}"}]}
start.wait(timeout=5)
status, body, _ = self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.assertEqual(status, 200)
self.assertIn(b'"kopru":1' if external else b'"ok":1', body)
with ThreadPoolExecutor(max_workers=50) as pool:
list(pool.map(call, range(50)))
native = [json.loads(x['govde']) for x in self.ozgun.gelen]
added = [json.loads(x['govde']) for x in self.kopru.gelen if x['yol'].endswith('/responses')]
self.assertEqual(len(native), 25)
self.assertEqual(len(added), 25)
self.assertTrue(all(x['model'] == 'gpt-6-astra' for x in native))
self.assertTrue(all(x['model'] == 'ide/test' for x in added))
self.assertEqual(len({x['prompt_cache_key'] for x in native + added}), 50)
self.assertEqual(bridge.Sunucu.request_queue_size, 128)
def test_concurrent_native_chat_is_not_blocked_by_external_preparation(self):
entered, release = threading.Event(), threading.Event()
original_worker = self.kopru.cevap
def worker(h, body):
if h.path.endswith('/prepare-transfer'):
entered.set()
if not release.wait(5):
return duz(422, b'{"error":{"message":"test wait expired"}}')(h, body)
return original_worker(h, body)
self.kopru.cevap = worker
external = {"model": "ide/test", "prompt_cache_key": "external-thread", "input": []}
native = {"model": "gpt-6-astra", "prompt_cache_key": "native-thread", "input": []}
with ThreadPoolExecutor(max_workers=2) as pool:
pending = pool.submit(self.iste, 'POST', '/backend-api/codex/responses', json.dumps(external).encode())
try:
self.assertTrue(entered.wait(2))
result = pool.submit(self.iste, 'POST', '/backend-api/codex/responses', json.dumps(native).encode()).result(timeout=2)
self.assertEqual(result[0], 200)
self.assertEqual(json.loads(self.ozgun.gelen[-1]['govde']), native)
self.assertEqual(len(self.kopru.gelen), 1)
finally:
release.set()
self.assertEqual(pending.result(timeout=3)[0], 200)
self.assertEqual(len(self.ozgun.gelen), 1)
def test_native_voice_never_enters_added_model_or_preparation_path(self):
for path in ("/backend-api/codex/realtime/calls", "/wham/realtime/calls?intent=quicksilver&architecture=avas"):
for model in ("ide/GPT-6-Astra", "ide/agy-gemini-3.8-flash-high", "gpt-6-astra"):
body = json.dumps({"model": model}).encode()
self.assertEqual(self.iste("POST", path, body, {"Authorization": "Bearer voice-test"})[0], 200)
self.assertEqual(self.ozgun.gelen[-1]["govde"], body)
self.assertEqual(self.ozgun.gelen[-1]["baslik"]["Authorization"], "Bearer voice-test")
self.assertEqual(self.kopru.gelen, [])
def test_returning_compressed_history_repaired_but_inference_stays_native(self):
req = {"model": "gpt-6-astra", "input": [ozet_kopru.ozet_ogesi("KARAR-4827"),
{"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "p", "name": "probe", "input": "x"}]}
body = zstd(json.dumps(req).encode())
self.iste("POST", "/backend-api/codex/responses", body, {"Content-Encoding": "zstd", "Authorization": "Bearer native-test"})
sent = self.ozgun.gelen[-1]
parsed = json.loads(sent["govde"])
self.assertIn("KARAR-4827", json.dumps(parsed))
self.assertEqual(parsed["input"][1]["id"], "ctc_call_173683")
self.assertNotIn("Content-Encoding", sent["baslik"])
self.assertEqual(sent["baslik"]["Authorization"], "Bearer native-test")
self.assertEqual(self.kopru.gelen, [])
def test_large_return_uses_preparation_only_not_aiturk_inference(self):
req = {"model": "gpt-6-astra", "input": [{"role": "assistant", "id": "msg_chatcmpl-foreign", "content": "x" * 100000},
{"role": "user", "content": "latest"}]}
def worker(h, body):
payload = json.loads(body)
self.assertEqual(h.path, "/aiturk/codex-baglama/prepare-transfer")
clean = dict(payload["request"], input=[{"role": "user", "content": "prepared summary"}, req["input"][-1]])
duz(200, json.dumps({"request": clean}).encode())(h, body)
self.kopru.cevap = worker
self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
self.assertEqual(len(self.kopru.gelen), 1)
self.assertEqual(json.loads(self.ozgun.gelen[-1]["govde"])["model"], "gpt-6-astra")
def test_preparation_failure_never_sends_truncated_request(self):
self.kopru.cevap = duz(422, b'{"error":{"message":"summary unavailable"}}')
_, body, _ = self.iste("POST", "/backend-api/codex/responses", b'{"model":"ide/test","input":[]}')
self.assertIn(b"response.failed", body)
self.assertEqual(self.ozgun.gelen, [])
self.assertEqual(len(self.kopru.gelen), 1)
if __name__ == "__main__":
unittest.main()
@@ -50,6 +50,12 @@ class Yapilandirma(unittest.TestCase):
class Secim(unittest.TestCase):
def test_full_catalog_up_to_one_thousand_models(self):
catalog = [girdi(f"provider-{n:04}", f"provider/model-{n:04}") for n in range(1100)]
selected = c.sec(catalog)
self.assertEqual(len(selected), 1000)
self.assertEqual(selected[-1]["id"], "provider-0999")
def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Uye katalogu yerel config'teki adlarla sinirlanmaz.
@@ -0,0 +1,81 @@
import os
import tempfile
import unittest
import ara
import internet
SAYFA = '''
<div class="result results_links results_links_deep web-result ">
<h2 class="result__title"><a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fornek.com%2Fbir%3Fa%3D1&amp;rut=x">Bir <b>başlık</b></a></h2>
<a class="result__snippet" href="//x">Kısa &amp; öz <b>özet</b></a>
</div>
<div class="result results_links web-result ">
<a class="result__a" href="https://duckduckgo.com/y.js?ad=1">Reklam</a>
</div>
<div class="result results_links web-result ">
<a class="result__a" href="https://ornek.org/iki">İkinci</a>
</div>
'''
class AraTest(unittest.TestCase):
def test_results_are_parsed_and_ads_skipped(self):
self.assertEqual(ara.sonuclari_coz(SAYFA), [("Bir başlık", "https://ornek.com/bir?a=1", "Kısa & öz özet"),
("İkinci", "https://ornek.org/iki", "")])
self.assertEqual(len(ara.sonuclari_coz(SAYFA, adet=1)), 1)
def test_search_output_and_empty_result(self):
metin = ara.ara("x", indirici=lambda adres: (SAYFA, "text/html"))
self.assertIn("1. Bir başlık\n https://ornek.com/bir?a=1", metin)
self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ('<div class="no-results">no results</div>', "text/html")))
def test_bot_challenge_uses_one_rss_backup_not_false_no_results(self):
calls = []
def indirici(adres):
calls.append(adres)
return (('<form id="challenge-form">Bot challenge</form>' if len(calls) == 1 else
'<rss><channel><item><title>OmniRoute</title><link>https://ornek.com/</link></item></channel></rss>'), 'text/html')
self.assertIn('https://ornek.com/', ara.ara('OmniRoute', indirici=indirici))
self.assertEqual(len(calls), 2)
def test_two_invalid_services_report_failure_not_no_results(self):
with self.assertRaisesRegex(RuntimeError, 'Arama hizmeti'):
ara.ara('x', indirici=lambda adres: ('<html>challenge</html>', 'text/html'))
def test_page_is_turned_into_text_and_cut(self):
sayfa = "<html><head><style>a{}</style><script>x()</script></head><body><nav>menü</nav><p>Bir &amp; iki</p><div>üç</div></body></html>"
self.assertEqual(ara.oku("ornek.com", indirici=lambda adres: (sayfa, "text/html")), "Bir & iki\nüç")
uzun = ara.oku("https://ornek.com", en_cok=10, indirici=lambda adres: ("<p>" + "a" * 50 + "</p>", "text/html"))
self.assertTrue(uzun.startswith("a" * 10 + "\n[... kesildi: 50"))
def test_main_reports_errors_without_raising(self):
self.assertEqual(ara.main(["oku", "http://127.0.0.1:9/yok"]), 1)
class InternetTest(unittest.TestCase):
def test_note_names_both_commands_and_is_added_once(self):
with tempfile.TemporaryDirectory() as d:
betik = os.path.join(d, "ara.py")
open(betik, "w").close()
metin = internet.talimat(None, betik, python="py.exe")
on = '& ' if os.name == 'nt' else ''
self.assertIn(f'{on}"py.exe" "{betik}" ara "sorgu"', metin)
self.assertIn(f'"py.exe" "{betik}" oku <adres>', metin)
req = {"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "araştır"}]}]}
self.assertTrue(internet.ekle(req, metin))
self.assertFalse(internet.ekle(req, metin))
self.assertEqual(req["input"][0]["role"], "developer")
self.assertEqual(len(req["input"]), 2)
self.assertIsNone(internet.talimat({"enabled": False}, betik))
self.assertIsNone(internet.talimat(None, os.path.join(d, "ara.py"))) # betik yoksa not eklenmez
def test_windowless_python_is_swapped_for_the_console_one(self):
with tempfile.TemporaryDirectory() as d:
open(os.path.join(d, "python.exe"), "w").close()
self.assertEqual(internet.python_yolu(os.path.join(d, "pythonw.exe")), os.path.join(d, "python.exe"))
self.assertEqual(internet.python_yolu(os.path.join("yok", "pythonw.exe")), os.path.join("yok", "pythonw.exe"))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,71 @@
"""The "send larger work to AITURK" instruction reaches only Codex Desktop, and only when a send command exists."""
import unittest
import is_hatti as h
class Hat(unittest.TestCase):
def test_only_the_desktop_app_is_recognised(self):
self.assertTrue(h.masaustu_mu("Codex Desktop"))
for other in ("codex-tui", "codex_exec", "codex_cli_rs", "", None, "Codex Desktop Helper"):
self.assertFalse(h.masaustu_mu(other), other)
def test_no_instruction_when_switched_off_or_without_a_send_command(self):
self.assertIsNone(h.talimat(None))
self.assertIsNone(h.talimat({}))
self.assertIsNone(h.talimat({"enabled": True, "komut": " "}))
self.assertIsNone(h.talimat({"enabled": False, "komut": "aiturk gonder"}))
self.assertIsNone(h.talimat({"enabled": False}, "C:/a/aiturk.exe", "s1"))
text = h.talimat({"enabled": True, "komut": "aiturk gonder --dosya is.md"})
self.assertIn("aiturk gonder --dosya is.md", text)
self.assertIn("sen yap", text)
self.assertEqual(h.talimat({"enabled": True, "komut": "x", "talimat": "ozel {komut}"}), "ozel x")
def test_on_by_default_once_a_send_command_exists_on_this_computer(self):
text = h.talimat(None, "C:/Program/aiturk.exe", "019a-bc_d")
self.assertIn('"C:/Program/aiturk.exe" is --sohbet 019a-bc_d --dosya', text)
self.assertIn('"C:/Program/aiturk.exe" is --dosya', h.talimat({}, "C:/Program/aiturk.exe", ""))
# Sohbet kimligi komut satirina yazilir: kabuk karakteri tasiyamaz.
self.assertIn("--sohbet abrm-rf ", h.komut_satiri("x", 'a"b; rm -rf /'))
# Ayardaki komut bulunanin onune gecer.
self.assertIn("elle", h.talimat({"komut": "elle"}, "C:/Program/aiturk.exe"))
def test_the_first_candidate_that_knows_the_command_is_used(self):
import os
import tempfile
with tempfile.TemporaryDirectory() as d:
eski = os.path.join(d, "TurkServis", "AITURK", "program", "bin", "aiturk.cmd")
yeni = os.path.join(d, "TurkServis", "AITURK-is-hatti", "Scripts", "aiturk.exe")
for yol in (eski, yeni):
os.makedirs(os.path.dirname(yol))
open(yol, "w").close()
ortam = {"LOCALAPPDATA": d}
self.assertEqual(h.adaylar(ortam)[0], eski)
self.assertEqual(h.komut_bul(ortam, destek=lambda y: y == yeni), yeni)
self.assertEqual(h.komut_bul(ortam, destek=lambda y: True), eski)
self.assertIsNone(h.komut_bul(ortam, destek=lambda y: False))
class Sonuc:
returncode = 2
self.assertFalse(h.destekliyor(eski, calistir=lambda *a, **k: Sonuc()))
Sonuc.returncode = 0
self.assertTrue(h.destekliyor(eski, calistir=lambda *a, **k: Sonuc()))
self.assertFalse(h.destekliyor(os.path.join(d, "yok.exe"), calistir=lambda *a, **k: Sonuc()))
def patlar(*a, **k):
raise OSError("x")
self.assertFalse(h.destekliyor(eski, calistir=patlar))
def test_instruction_is_put_first_once_and_never_repeated(self):
req = {"input": [{"type": "message", "role": "user", "content": "selam"}]}
text = h.talimat({"enabled": True, "komut": "aiturk gonder"})
self.assertTrue(h.ekle(req, text))
self.assertEqual((req["input"][0]["role"], len(req["input"])), ("developer", 2))
self.assertFalse(h.ekle(req, text))
self.assertEqual(len(req["input"]), 2)
self.assertFalse(h.ekle({"input": "duz metin"}, text))
self.assertFalse(h.ekle(req, None))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,358 @@
"""Layer check (11 Oct 2026): Jev passes or blocks each step of Katman-1..3; a blocked step is redone by the next
layer. Katman-4 is never asked. No exceptions."""
import ast
import io
import json
import os
import tempfile
import time
import unittest
from pathlib import Path
from unittest.mock import Mock
import jev_denetim
import yarim
SOURCE = Path(__file__).with_name("kopru.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 method(name):
return next(n for n in handler.body if isinstance(n, ast.FunctionDef) and n.name == name)
def user(text):
return {"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}
def sse(*deltas):
out = b""
for d in deltas:
out += b"event: response.output_text.delta\ndata: " + json.dumps(
{"type": "response.output_text.delta", "delta": d}).encode() + b"\n\n"
return out + b'event: response.completed\ndata: {"type": "response.completed"}\n\n'
class Yanit:
status = 200
def __init__(self, body):
self.parts = [body[:len(body) // 2], body[len(body) // 2:]]
self.headers = {"Content-Type": "text/event-stream"}
def read1(self, n):
return self.parts.pop(0) if self.parts else b""
class Karar(unittest.TestCase):
def test_refusal_or_own_conditions_block(self):
self.assertTrue(jev_denetim.karar({"ret": 0.84, "sart": 0.1}))
self.assertTrue(jev_denetim.karar({"ret": 0.41, "sart": 0.95}))
self.assertTrue(jev_denetim.karar({"ret": 0.5, "sart": 0.0}))
def test_normal_answer_passes(self):
self.assertFalse(jev_denetim.karar({"ret": 0.05, "sart": 0.3}))
def test_no_exception_questions_exist(self):
self.assertEqual(set(jev_denetim.SORU), {"ret", "sart"})
class Katman(unittest.TestCase):
def test_a_non_layer_model_falls_through_every_layer_from_the_top(self):
self.assertEqual(jev_denetim.sonrakiler("Uncensored-Uygulama"),
["Katman-1", "Katman-2", "Katman-3", "Katman-4"])
self.assertEqual(jev_denetim.sonrakiler("ide/some-model"),
["Katman-1", "Katman-2", "Katman-3", "Katman-4"])
def test_a_selected_layer_only_tries_the_ones_after_it(self):
self.assertEqual(jev_denetim.sonrakiler("Katman-1"), ["Katman-2", "Katman-3", "Katman-4"])
self.assertEqual(jev_denetim.sonrakiler("Katman-2"), ["Katman-3", "Katman-4"])
self.assertEqual(jev_denetim.sonrakiler("Katman-3"), ["Katman-4"])
def test_last_layer_is_never_asked(self):
self.assertTrue(jev_denetim.muaf_mi("Katman-4"))
self.assertEqual(jev_denetim.sonrakiler(None), [])
class Istek(unittest.TestCase):
def test_last_user_request_is_taken_and_wrappers_are_skipped(self):
voice = user("<realtime_delegation>\n <input>siteyi yap</input>\n</realtime_delegation>")
self.assertEqual(jev_denetim.son_istek({"input": [user("eski"), voice]}), "siteyi yap")
hook = user("<hook_context>pano</hook_context>")
call = {"type": "function_call", "call_id": "c", "name": "shell", "arguments": "{}"}
self.assertEqual(jev_denetim.son_istek({"input": [user("README'yi düzelt"), call, hook]}), "README'yi düzelt")
self.assertEqual(jev_denetim.son_istek({"input": [call]}), "")
class Sor(unittest.TestCase):
def test_request_shape_and_scores(self):
seen = {}
def gonder(govde, key):
seen.update(govde=govde, key=key)
return {"answers": {k: {"type": "noul", "noul": 0.25} for k in jev_denetim.SORU}}
self.assertEqual(jev_denetim.sor("istek", "c" * 9000, "anahtar", gonder=gonder), {"ret": 0.25, "sart": 0.25})
self.assertEqual(seen["key"], "anahtar")
self.assertEqual(seen["govde"]["state"]["user_request"], "istek")
self.assertEqual(seen["govde"]["state"]["assistant_reply"], "c" * 9000)
def test_failures_return_none_every_time(self):
calls = []
def bozuk(govde, key):
calls.append(1)
raise OSError("ulasilamadi")
for _ in range(5):
self.assertIsNone(jev_denetim.sor("i", "c", "k", gonder=bozuk))
self.assertEqual(len(calls), 5)
def test_key_is_read_from_env_or_file(self):
self.assertEqual(jev_denetim.anahtar({"TYPESAFE_API_KEY": " a "}), "a")
with tempfile.TemporaryDirectory() as d:
yol = os.path.join(d, "typesafe.env")
Path(yol).write_text('TYPESAFE_API_KEY="b"\n', encoding="utf-8")
self.assertEqual(jev_denetim.anahtar({}, [yol]), "b")
self.assertEqual(jev_denetim.anahtar({}, [os.path.join(d, "yok.env")]), "")
class Akis(unittest.TestCase):
"""_denetimli_akis through a fake handler: what reaches the client."""
def setUp(self):
self.logs, self.asked, self.opened = [], [], []
self.puanlar = [] # one verdict per asked answer; empty -> pass
sahte = Mock()
sahte.anahtar = lambda: "k"
sahte.sor = self.sor
sahte.karar = jev_denetim.karar
sahte.komutlar = jev_denetim.komutlar
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
sahte.muaf_mi = jev_denetim.muaf_mi
sahte.hata_gecis = jev_denetim.hata_gecis
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
"log": self.logs.append, "RESP_HOP": set(), "json": json}
adlar = ("_akis_basligi", "_denetimli_akis", "_stream")
exec(compile(ast.Module(body=[method(a) for a in adlar], type_ignores=[]), "kopru.py", "exec"), env)
self.h = type("H", (), {a: env[a] for a in adlar})()
self.h.wfile = io.BytesIO()
self.h.send_response = Mock()
self.h.send_header = Mock()
self.h.end_headers = Mock()
self.cevap = {"Katman-1": sse("bir"), "Katman-2": sse("iki"), "Katman-3": sse("uc"), "Katman-4": sse("dort")}
def sor(self, istek, metin, key):
self.asked.append(metin)
return self.puanlar.pop(0) if self.puanlar else {"ret": 0.0, "sart": 0.0}
def ac(self, ad):
self.opened.append(ad)
if self.cevap[ad] is None:
raise OSError("acilamadi")
return Yanit(self.cevap[ad]), None, f"[{ad}]"
def sent(self):
raw, out = self.h.wfile.getvalue(), b""
while raw:
size, _, rest = raw.partition(b"\r\n")
n = int(size, 16)
if n == 0:
break
out += rest[:n]
raw = rest[n + 2:]
return out
def calistir(self, body, sira=("Katman-2", "Katman-3", "Katman-4")):
self.h._denetimli_akis(Yanit(body), "[1]", None, None, "yap", list(sira), self.ac)
def test_a_passed_step_is_delivered_unchanged(self):
body = sse("Fenerbahçe'nin ", "13 puanı var.")
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, ["Fenerbahçe'nin 13 puanı var."])
self.assertEqual(self.opened, [])
def test_exact_voice_target_skips_jev_but_not_transport_fallback(self):
body = sse("Bunu yapamam.")
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.h._denetimli_akis(Yanit(body), "[1]", None, None, "yap",
list(jev_denetim.KATMAN), self.ac, hedef="Sesli-Hizli-Zeki")
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
self.assertEqual(self.opened, [])
def test_voice_transport_failure_still_checks_the_fallback_model(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.h._denetimli_akis(None, "[1]", None, None, "yap",
list(jev_denetim.KATMAN), self.ac, hedef="Sesli-Hizli-Zeki")
self.assertEqual(self.opened, ["Katman-1", "Katman-2"])
self.assertEqual(self.asked, ["bir", "iki"])
self.assertEqual(self.sent(), self.cevap["Katman-2"])
def test_voice_lookalikes_are_still_checked(self):
for target in ("sesli-hizli-zeki", "Sesli-Hizli-Zeki-v2", "ide/Sesli-Hizli-Zeki",
"Sesli-Hizli-Zeki ", "Other-Combo"):
with self.subTest(target=target):
self.setUp()
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.h._denetimli_akis(Yanit(sse("hayir")), "[1]", None, None, "yap",
list(jev_denetim.KATMAN), self.ac, hedef=target)
self.assertEqual(self.asked, ["hayir", "bir"])
self.assertEqual(self.opened, ["Katman-1"])
def test_a_blocked_non_layer_model_goes_to_layer_1_first(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}] # the selected (non-layer) model is blocked
self.calistir(sse("Bunu yapamam."), sira=("Katman-1", "Katman-2", "Katman-3", "Katman-4"))
self.assertEqual(self.sent(), self.cevap["Katman-1"])
self.assertEqual(self.opened, ["Katman-1"])
self.assertEqual(self.asked, ["Bunu yapamam.", "bir"])
def test_blocked_layer_1_goes_to_layer_2_which_is_checked_too(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.calistir(sse("Bunu yapamam."))
self.assertEqual(self.sent(), self.cevap["Katman-2"])
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
self.assertEqual(self.opened, ["Katman-2"])
self.assertEqual(self.asked, ["Bunu yapamam.", "iki"])
self.h.send_response.assert_called_once()
def test_every_layer_blocked_ends_at_layer_4_unasked(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}] * 3
self.calistir(sse("hayir"))
self.assertEqual(self.sent(), self.cevap["Katman-4"])
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
self.assertEqual(self.asked, ["hayir", "iki", "uc"]) # layer 4 is never asked
def test_blocked_layer_3_goes_straight_to_layer_4(self):
self.puanlar = [{"ret": 0.0, "sart": 0.8}]
self.calistir(sse("kendi sartimla"), sira=("Katman-4",))
self.assertEqual(self.sent(), self.cevap["Katman-4"])
self.assertEqual(self.asked, ["kendi sartimla"])
def test_a_layer_that_cannot_be_opened_is_skipped(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.cevap["Katman-2"] = None
self.calistir(sse("hayir"))
self.assertEqual(self.opened, ["Katman-2", "Katman-3"])
self.assertEqual(self.sent(), self.cevap["Katman-3"])
def test_a_layer_that_answers_with_an_error_is_skipped(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.cevap["Katman-2"] = b'event: error\ndata: {"type":"error","error":{"type":"upstream_error","message":"upstream 400"}}\n\n'
self.calistir(sse("hayir"))
self.assertEqual(self.opened, ["Katman-2", "Katman-3"])
self.assertEqual(self.sent(), self.cevap["Katman-3"])
self.assertNotIn(b"upstream 400", self.h.wfile.getvalue())
KOMUT = (b'event: response.output_item.done\ndata: {"type": "response.output_item.done", "item": '
b'{"type": "function_call", "name": "shell", "arguments": "{\\"cmd\\": \\"dir\\"}"}}\n\n')
def test_a_step_without_text_passes_without_jev(self):
body = self.KOMUT + sse()
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
self.assertEqual(self.opened, [])
def test_tool_step_cannot_be_replaced_even_if_jev_would_refuse(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
body = self.KOMUT + sse()
self.calistir(body)
self.assertEqual(self.opened, [])
self.assertEqual(self.asked, [])
self.assertEqual(self.sent(), body)
def test_tool_event_reaches_client_before_upstream_finishes(self):
first = (b'data: {"type":"response.output_item.added","item":'
b'{"type":"function_call","name":"shell","arguments":""}}\n\n')
owner = self
class Streaming(Yanit):
def __init__(self):
super().__init__(b'')
self.parts = [first, owner.KOMUT + sse()]
self.reads = 0
def read1(self, n):
self.reads += 1
if self.reads == 2:
owner.assertIn(first, owner.h.wfile.getvalue())
owner.assertEqual(owner.asked, [])
owner.assertEqual(owner.opened, [])
return super().read1(n)
self.h._denetimli_akis(Streaming(), '[1]', None, None, 'yap', ['Katman-1'], self.ac)
self.assertEqual(self.sent(), first + self.KOMUT + sse())
def test_text_with_a_tool_call_is_preparation_not_a_final_refusal(self):
body = sse('Önce dosyaları inceleyeceğim.') + self.KOMUT
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
self.assertEqual(self.opened, [])
def test_explicit_commentary_passes_without_jev(self):
body = (b'data: {"type":"response.output_item.added","item":'
b'{"type":"message","role":"assistant","phase":"commentary"}}\n\n' + sse('Araştırıyorum.'))
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.asked, [])
def test_an_empty_step_of_the_selected_model_goes_to_the_next_layer(self):
self.calistir(sse()) # no text, no tool call
self.assertEqual(self.asked, ["iki"]) # nothing to ask about the empty step itself
self.assertEqual(self.opened, ["Katman-2"])
self.assertEqual(self.sent(), self.cevap["Katman-2"])
def test_an_empty_layer_goes_on_to_the_one_after_it(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.cevap["Katman-2"] = sse()
self.calistir(sse("hayir"))
self.assertEqual(self.opened, ["Katman-2", "Katman-3"])
self.assertEqual(self.sent(), self.cevap["Katman-3"])
def test_an_error_of_the_selected_model_goes_to_the_next_layer(self):
self.h._denetimli_akis(None, "[1]", None, None, "yap", ["Katman-1", "Katman-2", "Katman-3", "Katman-4"], self.ac)
self.assertEqual(self.opened, ["Katman-1"])
self.assertEqual(self.sent(), self.cevap["Katman-1"])
def test_an_answer_cut_midway_goes_to_the_next_layer(self):
class Kopan(Yanit):
def read1(self, n):
if not self.parts:
raise OSError("koptu")
return self.parts.pop(0)
self.h._denetimli_akis(Kopan(sse("yarim")), "[1]", None, None, "yap", ["Katman-2", "Katman-3", "Katman-4"], self.ac)
self.assertEqual(self.opened, ["Katman-2"])
self.assertEqual(self.sent(), self.cevap["Katman-2"])
self.assertNotIn(b"yarim", self.h.wfile.getvalue())
def test_when_no_layer_opens_the_blocked_answer_is_not_delivered(self):
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
self.cevap.update({"Katman-2": None, "Katman-3": None, "Katman-4": None})
self.calistir(sse("Bunu yapamam."))
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
self.assertIn(b'response.failed', self.sent())
def test_the_last_layers_own_error_body_is_delivered_untouched(self):
class Hata(Exception):
def read(self):
return b'{"error": {"message": "katman 4 hatasi"}}'
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
def ac(ad):
raise Hata("HTTP 400")
self.h._denetimli_akis(Yanit(sse("Bunu yapamam.")), "[1]", None, None, "yap", ["Katman-4"], ac)
self.assertEqual(self.sent(), b'{"error": {"message": "katman 4 hatasi"}}')
def test_unreachable_jev_keeps_the_answer(self):
self.puanlar = [None]
body = sse("Bunu yapamam.")
self.calistir(body)
self.assertEqual(self.sent(), body)
self.assertEqual(self.opened, [])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,111 @@
import copy
import json
import os
import subprocess
import tempfile
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from unittest.mock import patch
import jev_denetim
import jev_settings
class Settings(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name)
self.home_patch = patch.object(jev_settings, 'home', return_value=self.home)
self.home_patch.start()
self.addCleanup(self.home_patch.stop)
self.settings = copy.deepcopy(jev_settings.DEFAULTS)
def save(self, **changes):
self.settings.update(changes)
(self.home / 'jev-settings.json').write_text(json.dumps({'settings': self.settings}), encoding='utf-8')
def test_live_rules_general_override_and_always_pass(self):
self.save(fallbackModel='B', rejectionRules=[{'from': 'A', 'to': 'C'}, {'from': 'C', 'to': 'D'}], exemptModels=['D'])
self.assertEqual(jev_denetim.sonrakiler('A'), ['C', 'D'])
self.assertEqual(jev_denetim.sonrakiler('Other'), ['B'])
self.assertTrue(jev_denetim.muaf_mi('D'))
self.assertFalse(jev_denetim.muaf_mi('A'))
self.save(enabled=False)
self.assertFalse(jev_denetim.etkin_mi())
self.assertEqual(jev_denetim.sonrakiler('A'), [])
def test_cycle_cannot_spin_even_if_file_manually_edited(self):
self.save(rejectionRules=[{'from': 'A', 'to': 'B'}, {'from': 'B', 'to': 'A'}])
self.assertEqual(jev_denetim.sonrakiler('A'), ['B'])
def test_prompts_model_criteria_and_thresholds_are_applied(self):
self.save(model='jev-custom', questions={'ret': 'question A', 'sart': 'question B'}, thresholds={'ret': .9, 'sart': .8}, criteria={'ret': {'true': 'bad', 'false': 'good'}, 'sart': {'true': '', 'false': ''}})
captured = []
def send(body, key):
captured.append(body)
return {'answers': {'ret': {'noul': .6}, 'sart': {'noul': .2}}}
result = jev_denetim.sor('request', 'answer', 'test', gonder=send)
self.assertFalse(jev_denetim.karar(result))
self.assertEqual(captured[0]['model'], 'jev-custom')
self.assertEqual(captured[0]['questions']['ret']['instructions'], 'question A')
self.assertEqual(captured[0]['questions']['ret']['criteria'], {'true': 'bad', 'false': 'good'})
self.assertNotIn('criteria', captured[0]['questions']['sart'])
self.save(thresholds={'ret': .1, 'sart': .1})
self.assertFalse(jev_denetim.karar(result)) # in-flight result retains its own thresholds
def test_concurrency_is_limited_without_serializing_all_requests(self):
self.save(maxConcurrent=2)
count, peak = 0, 0
lock = threading.Lock()
def send(body, key):
nonlocal count, peak
with lock:
count += 1
peak = max(peak, count)
time.sleep(.04)
with lock:
count -= 1
return {'answers': {'ret': {'noul': 0}, 'sart': {'noul': 0}}}
with ThreadPoolExecutor(max_workers=8) as pool:
answers = list(pool.map(lambda _: jev_denetim.sor('r', 'a', 'test', gonder=send), range(8)))
self.assertEqual(peak, 2)
self.assertTrue(all(a is not None for a in answers))
def test_timeout_endpoint_and_failure_behavior(self):
self.save(endpoint='https://example.test/jev', timeoutSeconds=42, failureAction='next')
with patch('urllib.request.urlopen', side_effect=TimeoutError) as opening:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test'))
self.assertEqual(opening.call_args.kwargs['timeout'], 42)
self.assertEqual(opening.call_args.args[0].full_url, 'https://example.test/jev')
self.assertTrue(jev_denetim.hata_gecis())
self.assertEqual(jev_settings._active, 0)
def test_queue_wait_expires_and_slot_count_is_preserved(self):
self.save(maxConcurrent=1, queueWaitSeconds=1)
settings = jev_settings.read()[0]
self.assertTrue(jev_settings.acquire(settings))
try:
with patch.object(jev_denetim, '_gonder') as send:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test', gonder=send))
send.assert_not_called()
self.assertEqual(jev_settings._active, 1)
finally:
jev_settings.release()
@unittest.skipUnless(os.name == 'nt', 'Windows DPAPI')
def test_encrypted_key_roundtrip_without_plaintext_on_disk(self):
secret = 'dummy-key-for-local-test'
code = 'Add-Type -AssemblyName System.Security; $b=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
encrypted = subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', code], input=secret, capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW).stdout.strip()
text = json.dumps({'settings': self.settings, 'protectedKey': encrypted})
(self.home / 'jev-settings.json').write_text(text, encoding='utf-8')
self.assertNotIn(secret, text)
self.assertEqual(jev_settings.key(), secret)
if __name__ == '__main__':
unittest.main()
@@ -7,8 +7,9 @@ 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"}
tree = ast.parse(Path(__file__).with_name("kopru.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
@@ -31,8 +32,11 @@ class Ek(unittest.TestCase):
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)
self.assertEqual(out[0]["type"], "response.failed")
self.assertIn("araç çağrısını desteklemiyor", out[0]["response"]["error"]["message"])
# 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"))
@@ -60,6 +64,25 @@ class Ek(unittest.TestCase):
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}
@@ -29,18 +29,44 @@ 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 yap", "ide/9router-cc-claude-opus-5-5"),
("modeli opusa geç", "ide/9router-cc-claude-opus-5-5"),
("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_half_spoken_words_and_the_plain_variant(self):
"""Measured 11 Oct 2026 in a voice session: "obli think" was not found three times, and "Qwen 3.8
Obliterated" asked which one although the user had not said "thinking"."""
qwen = MODELS + [("ide/9router-nano-qwen-qwen3.8-27b-obliterated", "nano/qwen3.8-27b-obliterated"),
("ide/9router-nano-qwen-qwen3.8-27b-obliterated-thinking", "nano/qwen3.8-27b-obliterated:thinking"),
("ide/9router-nano-qwen-qwen3.8-27b-uncensored", "nano/qwen3.8-27b-uncensored")]
def sec(text):
return (g.eslestir(g.komut_coz(text)["ad"], qwen) or {}).get("slug")
thinking = "ide/9router-nano-qwen-qwen3.8-27b-obliterated-thinking"
self.assertEqual(sec("Modeli 3.8 Obliterated Think yap"), thinking)
self.assertEqual(sec("Modeli Qwen... modeli Qwen 3.8 .27 B... Obli... Think yap"), thinking)
self.assertEqual(sec("Modeli Qwen ... 3.8 Obliterated yap"), "ide/9router-nano-qwen-qwen3.8-27b-obliterated")
self.assertEqual(sec("modeli klod opus yap"), "ide/9router-cc-claude-opus-5-5") # not the "(no-think)" one
self.assertIn("adaylar", g.eslestir(g.komut_coz("modeli qwen 3.8 yap")["ad"], qwen)) # obliterated or uncensored: ask
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 flash yap")) # several unrelated Flash models: 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",
"modeli değiştir", "hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
"hangi modeli önerirsin bu iş için bana söyle bakalım lütfen",
"model " + "çok uzun bir cümle " * 5 + "yap"):
self.assertFalse(pick(text) and "slug" in pick(text), text)
self.assertEqual(g.komut_coz("seçili model hangisi"), {"tur": "sor"})
@@ -77,7 +103,7 @@ class TurOrtasi(unittest.TestCase):
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 yap"), user("modeli gpt 6 astra yap")]}
voice("modeli opus 5 5 yap"), user("modeli gpt 6 astra yap")]}
self.assertEqual(g.gecmisi_temizle(req, MODELS), 2)
self.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")
@@ -86,6 +112,44 @@ class Gecmis(unittest.TestCase):
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:
@@ -6,7 +6,7 @@ from pathlib import Path
import unittest
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
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]
@@ -0,0 +1,357 @@
"""Real pipes/processes and real loopback HTTP; no native runtime activation."""
import concurrent.futures
import http.client
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import native_speech_mux as muxmod
# Fake engine is a separate process speaking complete JSONL, not mocked Popen.
ENGINE = r'''
import json, os, sys, time
if '--probe' in sys.argv:
print(json.dumps({'argv':sys.argv[1:], 'cwd':os.getcwd(), 'marker':os.getenv('MUX_TEST_MARKER')}))
sys.stderr.write('original-stderr\n')
sys.exit(23)
for raw in sys.stdin.buffer:
try: m=json.loads(raw)
except ValueError:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush(); continue
method=m.get('method')
def emit(v):
sys.stdout.buffer.write(json.dumps(v).encode()+b'\n'); sys.stdout.buffer.flush()
if method=='thread/start':
emit({'id':m['id'],'result':{'thread':{'id':'thread-A'}}})
elif method=='thread/realtime/start':
emit({'method':'thread/realtime/started','params':{'threadId':'thread-A','realtimeSessionId':'rt'}})
elif method=='thread/realtime/stop':
emit({'method':'thread/realtime/closed','params':{'threadId':'thread-A','reason':None}})
elif method=='error-event':
emit({'method':'thread/realtime/error','params':{'threadId':'thread-A','message':'failed'}})
elif method=='thread/realtime/appendSpeech':
emit({'method':'item/commandExecution/requestApproval','id':m['id'],'params':{'command':'test'}})
emit({'method':'seen-speech','params':m['params']})
emit({'id':m['id'], 'error':{'code':-1,'message':'no'}} if m['params']['text']=='rpc-error' else {'id':m['id'],'result':{}})
elif method=='delay':
time.sleep(.25)
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
elif method=='pause':
emit({'method':'paused'})
while not os.path.exists(m['params']['release']): time.sleep(.01)
emit({'method':'released'})
elif method=='exit':
sys.exit(17)
else:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
sys.stdout.buffer.write(b'{"method":"saw-eof"}\n'); sys.stdout.buffer.flush()
sys.stderr.write('engine-eof\n')
sys.exit(19)
'''
class Harness:
def __init__(self, tmp_path):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
self.mux = muxmod.NativeSpeechMux([sys.executable, '-u', str(engine), 'app-server'], observer=True)
ir, iw = os.pipe()
outr, outw = os.pipe()
self.source = os.fdopen(ir, 'rb', buffering=0)
self.input = os.fdopen(iw, 'wb', buffering=0)
self.target = os.fdopen(outw, 'wb', buffering=0)
self.output = os.fdopen(outr, 'rb')
self.lines = queue.Queue()
self.code = None
self.error = None
self.url, self.token = self.mux.start_inlet()
def run():
try:
self.code = self.mux.run(self.source, self.target)
except BaseException as e:
self.error = e
finally:
self.target.close()
self.thread = threading.Thread(target=run, daemon=True)
self.thread.start()
def collect():
for raw in self.output:
self.lines.put(raw)
self.collector = threading.Thread(target=collect, daemon=True)
self.collector.start()
def send(self, value):
raw = value if isinstance(value, bytes) else json.dumps(value).encode()+b'\n'
self.input.write(raw)
def recv(self):
return self.lines.get(timeout=8)
def http(self, path, body, token=None):
conn = http.client.HTTPConnection('127.0.0.1', int(self.url.rsplit(':', 1)[1]), timeout=5)
conn.request('POST', path, json.dumps(body), {'Authorization':'Bearer '+(token if token is not None else self.token)})
response = conn.getresponse()
data = response.read()
conn.close()
return response.status, json.loads(data) if response.status == 200 else data
def start(self):
self.send({'id':1, 'method':'thread/start','params':{}})
assert json.loads(self.recv())['result']['thread']['id'] == 'thread-A'
assert self.mux.snapshot()['threads']['thread-A']['active'] is False
self.send({'id':2,'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
assert json.loads(self.recv())['method'] == 'thread/realtime/started'
return self.bind()
def bind(self):
generation = self.mux.snapshot()['threads']['thread-A']['generation']
b = muxmod.Binding('tenant','bekci','rev',self.mux.connection_id,'thread-A',generation)
assert self.mux.bind(b)
return self.notice(generation)
def notice(self, generation=1, **changes):
now = time.time()
return dict(notice_id='notice-1',tenant_id='tenant',bekci_session_id='bekci',history_revision='rev',
connection_id=self.mux.connection_id,thread_id='thread-A',expected_generation=generation,
issued_at=now,expires_at=now+30,text='düzeltme', **changes)
def close(self):
self.input.close()
self.thread.join(10)
if self.thread.is_alive():
self.mux._process.kill()
self.thread.join(5)
raise AssertionError('mux failed to exit after EOF')
self.collector.join(3)
self.source.close()
self.output.close()
assert self.error is None
def test_native_notice_rpc_and_approval_are_separate(tmp_path):
h = Harness(tmp_path)
try:
notice = h.start()
assert h.http('/notice', notice, 'wrong')[0] == 401
assert h.http('/notice', notice)[1]['status'] == 'queued'
approval = json.loads(h.recv())
assert approval['method'] == 'item/commandExecution/requestApproval'
assert approval['id'].startswith('bekci-speech-')
assert json.loads(h.recv()) == {'method':'seen-speech','params':{'threadId':'thread-A','text':'düzeltme'}}
answer = (json.dumps({'id':approval['id'],'result':{'decision':'decline'}}, separators=(', ', ': '))+'\r\n').encode()
h.send(answer)
assert h.recv() == answer
assert h.http('/notice', notice)[1]['status'] == 'rpc_accepted'
assert h.lines.empty() # native own-result never leaked to desktop
assert h.http('/rpc', {'method':'turn/start'})[0] == 404
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'queued'
h.recv(); h.recv()
h.send(b'{"method":"barrier"}\n')
assert h.recv() == b'{"method":"barrier"}\n'
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'rpc_error'
finally:
h.close()
assert h.code == 19
assert json.loads(h.recv())['method'] == 'saw-eof'
def test_unbound_schema_ttl_tenant_generation_and_restart(tmp_path):
h = Harness(tmp_path)
try:
n = h.notice()
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
n = h.start()
for delta, status in [({'method':'turn/start'},'invalid_schema'),
({'expected_generation':True},'invalid_schema'),
({'expires_at':time.time()-1},'expired_or_invalid_ttl'),
({'issued_at':float('nan')},'expired_or_invalid_ttl'),
({'tenant_id':'other'},'binding_not_established'),
({'history_revision':'other'},'binding_mismatch'),
({'expected_generation':2},'binding_mismatch'),
({'connection_id':'other'},'binding_mismatch')]:
assert h.http('/notice', {**n, **delta})[1]['status'] == status
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
assert json.loads(h.recv())['method'] == 'thread/realtime/closed'
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
h.recv()
fresh = h.bind()
assert fresh['expected_generation'] == n['expected_generation']+1
assert h.http('/notice', n)[1]['status'] == 'binding_mismatch'
h.send({'method':'error-event'})
h.recv()
assert h.http('/notice', fresh)[1]['status'] == 'binding_not_established'
finally:
h.close()
def test_concurrent_notices_and_raw_backpressure_order(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
h.send({'method':'delay'})
records = [(json.dumps({'method':'echo','i':i,'padding':'x'*16000})+'\r\n').encode() for i in range(90)]
producer = threading.Thread(target=lambda: [h.send(r) for r in records], daemon=True)
producer.start()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(lambda i: h.http('/notice',{**n,'notice_id':str(i)})[1]['status'], range(12)))
assert set(results) <= {'queued','busy'}
accepted = results.count('queued')
echoes = []
speech = 0
approvals = 0
for _ in range(1 + len(records) + accepted*2):
raw = h.recv()
method = json.loads(raw).get('method')
if method == 'echo': echoes.append(raw)
elif method == 'seen-speech': speech += 1
elif method == 'item/commandExecution/requestApproval': approvals += 1
producer.join(5)
assert not producer.is_alive()
assert echoes == records
assert speech == approvals == accepted
assert h.mux._queue.maxsize == 32
finally:
h.close()
def test_fragmented_oversized_jsonl_and_partial_eof_preserved(tmp_path):
h = Harness(tmp_path)
try:
h.start()
raw = b'{ "method":"huge", "value":"' + b'x'*200000 + b'"}\r\n'
producer = threading.Thread(target=lambda: h.send(raw),daemon=True)
producer.start()
assert h.recv() == raw
producer.join(5)
assert h.mux.snapshot()['injection_disabled']
h.send(b'{"partial":true}')
finally:
h.close()
# Engine echoes an unterminated record, then its EOF notification: byte exact.
assert h.recv() == b'{"partial":true}{"method":"saw-eof"}\n'
assert h.code == 19
def test_non_appserver_passthrough_argv_env_cwd_stderr_exit(tmp_path, monkeypatch):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
module = Path(muxmod.__file__).resolve()
monkeypatch.setenv('MUX_TEST_MARKER','inherited')
args = [sys.executable,str(module),sys.executable,'--','-u',str(engine),'--probe','space arg','app-server']
p = subprocess.run(args,cwd=tmp_path,capture_output=True,timeout=10)
assert p.returncode == 23
result = json.loads(p.stdout)
assert result == {'argv':['--probe','space arg','app-server'],'cwd':str(tmp_path),'marker':'inherited'}
direct = subprocess.run([sys.executable, '-u', str(engine), '--probe', 'space arg', 'app-server'],
cwd=tmp_path, capture_output=True, timeout=10)
assert (p.stdout, p.stderr, p.returncode) == (direct.stdout, direct.stderr, direct.returncode)
def test_engine_exit_without_desktop_eof(tmp_path):
h = Harness(tmp_path)
try:
h.send({'method':'exit'})
h.thread.join(5)
assert not h.thread.is_alive()
assert h.code == 17
assert h.mux.snapshot()['closed']
finally:
h.close()
def test_owned_namespace_collision_disables_injection_but_forwards(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
raw = json.dumps({'id':h.mux._prefix+'collision','method':'echo'}).encode()+b'\n'
h.send(raw)
assert h.recv() == raw
assert h.http('/notice', n)[1]['status'] == 'injection_disabled'
finally:
h.close()
def test_queued_notice_revalidated_after_observed_stop(tmp_path):
h = Harness(tmp_path)
release = tmp_path / 'release'
try:
n = h.start()
h.send({'method':'pause','params':{'release':str(release)}})
assert json.loads(h.recv())['method'] == 'paused'
# Fill the OS pipe while the real engine is paused; leave bounded queue
# capacity for the notice and stop record behind it.
for i in range(8):
h.send({'method':'echo','i':i,'padding':'x'*32000})
assert h.http('/notice', n)[1]['status'] == 'queued'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
deadline = time.monotonic()+5
while h.mux.snapshot()['threads']['thread-A']['active'] and time.monotonic() < deadline:
time.sleep(.01)
assert not h.mux.snapshot()['threads']['thread-A']['active']
release.touch()
methods = [json.loads(h.recv())['method'] for _ in range(10)]
assert methods == ['released'] + ['echo']*8 + ['thread/realtime/closed']
assert h.mux._receipts[('tenant','bekci','notice-1')]['status'] == 'binding_not_established'
assert h.lines.empty()
finally:
release.touch()
h.close()
def test_real_request_correlation_and_opaque_receipt_contract(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
adapter = muxmod.WatchReceiptAdapter(h.mux)
headers = {'Session-Id':'thread-A','Thread-Id':'thread-A'}
correlation = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
assert correlation.thread_id == 'thread-A'
assert adapter.capture({**headers,'Thread-Id':'other'}, {'prompt_cache_key':'thread-A'}) is None
assert adapter.capture(headers, {}) is None
assert adapter.accept('bekci.watch', {}, correlation) == 'backend_contract_not_established'
# Internal contract fixture, explicitly NOT a claim about backend wire format.
receipt = muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()+60)
adapter.decode = lambda payload: payload
adapter.resolve = lambda handle: receipt
assert adapter.accept('bekci.watch','opaque',correlation) == 'bound'
assert adapter.accept('_bekci_watch','opaque',correlation) == 'bound'
assert adapter.accept('other','opaque',correlation) == 'unsupported_metadata_transport'
assert adapter.notice('unbound',n)['status'] == 'binding_not_established'
assert adapter.notice('opaque',{**n,'tenant_id':'other'})['status'] == 'binding_mismatch'
assert adapter.notice('opaque',n)['status'] == 'queued'
assert json.loads(h.recv())['method'] == 'item/commandExecution/requestApproval'
assert json.loads(h.recv())['method'] == 'seen-speech'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}}); h.recv()
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}}); h.recv()
assert adapter.accept('bekci.watch','opaque',correlation) == 'stale_generation'
fresh = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','wrong',time.time()+60)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()-1)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
finally:
h.close()
def test_verified_original_launcher_fallback_actual_process(tmp_path):
import hashlib
executable = str(Path(sys.executable).resolve())
with open(executable,'rb') as f:
digest = hashlib.file_digest(f,'sha256').hexdigest()
assert muxmod.verified_original(executable,digest) == executable
import pytest
with pytest.raises(ValueError): muxmod.verified_original(executable,'0'*64)
with pytest.raises(ValueError): muxmod.verified_original('python.exe',digest)
# A value that happens to say app-server must not trigger observer/attachment.
marker = tmp_path/'original-ran'
code = 'from pathlib import Path; import sys; Path(sys.argv[1]).write_text("ok"); sys.exit(21)'
def forbidden(adapter):
raise AssertionError('not an app-server invocation')
assert muxmod.launch_original(executable,digest,['-c',code,str(marker),'app-server'],attach=forbidden) == 21
assert marker.read_text() == 'ok'
@@ -0,0 +1,115 @@
"""Windows owned-process lifetime tests; no installed process is terminated."""
import ctypes
from ctypes import wintypes as w
import hashlib
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import pytest
from prepare_native_watch import prepare
pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason='Windows executable lifecycle')
def descendants(parent):
class Entry(ctypes.Structure):
_fields_=[('size',w.DWORD),('usage',w.DWORD),('pid',w.DWORD),('heap',ctypes.c_size_t),('module',w.DWORD),('threads',w.DWORD),('parent',w.DWORD),('priority',w.LONG),('flags',w.DWORD),('exe',w.WCHAR*260)]
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Process32FirstW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.Process32NextW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.CloseHandle.argtypes=[w.HANDLE]
snap=k.CreateToolhelp32Snapshot(2,0);entry=Entry();entry.size=ctypes.sizeof(entry);rows=[]
try:
more=k.Process32FirstW(snap,ctypes.byref(entry))
while more:
rows.append((entry.pid,entry.parent));more=k.Process32NextW(snap,ctypes.byref(entry))
finally:k.CloseHandle(snap)
owned={parent}
while True:
children={pid for pid,ppid in rows if ppid in owned}
if children <= owned: return owned-{parent}
owned |= children
def assert_abrupt_cleanup(p, expected_min):
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.WaitForSingleObject.argtypes=[w.HANDLE,w.DWORD]
k.CloseHandle.argtypes=[w.HANDLE]
ids=descendants(p.pid)
assert len(ids)>=expected_min, ids
handles=[k.OpenProcess(0x100000,False,pid) for pid in ids]
assert all(handles)
try:
p.kill();p.wait(timeout=5)
assert all(k.WaitForSingleObject(h,5000)==0 for h in handles), ids
finally:
for h in handles:k.CloseHandle(h)
return len(ids)
def line(p):
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
return q.get(timeout=20)
def candidate(tmp_path, original, digest):
return prepare(Path(__file__).parent,tmp_path/'candidate',original,digest,Path(sys.executable),tmp_path/'bridge',tmp_path/'agent')
def test_abrupt_bootstrap_exit_kills_owned_python_and_grandchild(tmp_path):
exe=Path(sys.executable)
launcher=candidate(tmp_path,exe,hashlib.sha256(exe.read_bytes()).hexdigest())
script='import subprocess,sys,time; p=subprocess.Popen([sys.executable,"-c","import time; time.sleep(120)"]); print(p.pid,flush=True); time.sleep(120)'
p=subprocess.Popen([str(launcher),'-u','-c',script],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
assert int(line(p))>0
assert_abrupt_cleanup(p,3)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_pinned_original_initialize_and_abrupt_cleanup(tmp_path):
# Explicit provisioned evidence, never discovers or kills an existing app.
manifest_path=os.environ.get('NATIVE_WATCH_TEST_MANIFEST')
if not manifest_path: pytest.skip('requires explicit pinned original candidate')
manifest=json.loads(Path(manifest_path).read_text(encoding='utf-8-sig'))
launcher=candidate(tmp_path,Path(manifest['original']),manifest['original_sha256'])
p=subprocess.Popen([str(launcher),'-c','model_provider="openai"','app-server','--analytics-default-enabled'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
p.stdin.write(json.dumps({'id':1,'method':'initialize','params':{'clientInfo':{'name':'owned-cleanup-proof','version':'1.0'},'capabilities':{'experimentalApi':True}}}).encode()+b'\n');p.stdin.flush()
response=json.loads(line(p));assert response.get('id')==1 and 'result' in response,response
assert_abrupt_cleanup(p,2)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_compiled_launcher_fallback(tmp_path):
source=Path(__file__).parent
import hashlib
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(source,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
args=['-c','import json,sys,os; print(json.dumps([sys.argv[1:],os.getcwd()])); sys.stderr.write("stderr\\n"); sys.exit(23)','space arg','quote"arg','trailing\\']
direct=subprocess.run([str(exe),*args],cwd=tmp_path,capture_output=True)
wrapped=subprocess.run([str(launcher),*args],cwd=tmp_path,capture_output=True)
assert (wrapped.returncode,wrapped.stdout,wrapped.stderr)==(direct.returncode,direct.stdout,direct.stderr)
manifest=launcher.with_suffix('.json');data=json.loads(manifest.read_text(encoding='utf-8'));data['original_sha256']='0'*64;manifest.write_text(json.dumps(data),encoding='utf-8')
assert subprocess.run([str(launcher),'--version'],capture_output=True).returncode==78
def test_executable_flushes_interactive_stdin_before_eof(tmp_path):
import hashlib,queue
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(Path(__file__).parent,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
p=subprocess.Popen([str(launcher),'-u','-c','import sys; print(sys.stdin.readline().strip(),flush=True); sys.stdin.read()'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
try:
p.stdin.write(b'live-before-eof\n');p.stdin.flush()
assert q.get(timeout=5).strip()==b'live-before-eof'
finally:
p.stdin.close();p.wait(timeout=5)
@@ -10,14 +10,14 @@ import ide_catalog as c
import pencere
import resim_context
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
tree = ast.parse(Path(__file__).with_name("kopru.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"), "log": lambda message: None, "pencere": pencere}
env = {"json": json, "re": __import__("re"), "time": __import__("time"), "log": lambda message: None, "pencere": pencere}
exec(compile(ast.Module(body=nodes, type_ignores=[]), "bridge.py", "exec"), env)
NANO = pencere.nano_sadelestir({"data": [
@@ -196,9 +196,13 @@ class GatewayErrors(unittest.TestCase):
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[0]["type"], "response.failed")
self.assertEqual(out[0]["response"]["error"]["code"], "insufficient_quota")
self.assertIn("upstream 402", out[0]["response"]["error"]["message"])
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")
@@ -225,5 +229,25 @@ class Images(unittest.TestCase):
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()
@@ -3,7 +3,7 @@ import ast
import unittest
from pathlib import Path
tree = ast.parse(Path(__file__).with_name("bridge.py").read_text(encoding="utf-8"))
tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
wanted = {"_zstd_decompress", "_zstd_ready", "decompress"}
env = {}
exec(compile(ast.Module(body=[n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in wanted],
+17 -2
View File
@@ -36,6 +36,8 @@ class Izleyici:
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:
@@ -48,7 +50,15 @@ class Izleyici:
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:
self.bitti = True
# Bitis, olayin TURUNDEN anlasilir; arac argumaninda gecen "response.completed" sozu bitis degildir.
for satir in blok.split(b"\n"):
if satir.startswith(b"data:"):
try:
tur = json.loads(satir[5:]).get("type")
except (ValueError, AttributeError):
continue
if tur in ("response.completed", "response.failed", "response.incomplete"):
self.bitti = True
@property
def metin(self):
@@ -89,7 +99,12 @@ def uygula(req, anahtar, simdi=None):
girdi = req.get("input")
if not kayit or not isinstance(girdi, list) or not girdi:
return 0
son = girdi[-1]
son = None
for oge in reversed(girdi): # kancalarin kullanici mesajinin ardina ekledigi talimat mesajlari atlanir
if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") in ("developer", "system"):
continue
son = oge
break
if not (isinstance(son, dict) and son.get("type", "message") == "message" and son.get("role") == "user"):
return 0 # sikistirma istegi, arac sonucu turu vb.: yeni kullanici mesajini bekle
metin = kayit["metin"]
+1 -1
View File
@@ -6,7 +6,7 @@ export const AITURK_PRODUCT = Object.freeze({
protocol: 'aiturk-ide',
website: 'https://turkservis.online',
downloads: 'https://turkservis.online/ide',
repository: 'https://gitea.twinpay.one/yilsem/aiturk-hermes-ide',
repository: 'https://gitea.turkservis.online/yilsem/aiturk-hermes-ide',
apiBaseUrl: 'https://ai.turkservis.online/v1'
})
+1 -1
View File
@@ -234,7 +234,7 @@ function downloadInstallScript(ref, destPath) {
// placeholder is a real GitHub commit.
const scriptName = installScriptName()
const refType = isPinnedCommit(ref) ? 'commit' : 'branch'
const url = `https://gitea.twinpay.one/yilsem/aiturk-hermes-ide/raw/${refType}/${encodeURIComponent(ref)}/scripts/${scriptName}`
const url = `https://gitea.turkservis.online/yilsem/aiturk-hermes-ide/raw/${refType}/${encodeURIComponent(ref)}/scripts/${scriptName}`
return new Promise((resolve, reject) => {
fs.mkdirSync(path.dirname(destPath), { recursive: true })
@@ -1,7 +1,13 @@
import { describe, expect, it } from 'vitest'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {
addCodexLinkLine,
CODEX_BRIDGE_VERSION,
CODEX_LINK_LINE,
CODEX_LINK_RUN_COMMAND_MAX,
codexAutostartPlan,
@@ -34,6 +40,23 @@ function expectRoundTrip(original: string) {
}
describe('the contract line', () => {
it('expects the version exported by the bundled Python bridge', () => {
const home = mkdtempSync(path.join(tmpdir(), 'codex-bridge-version-'))
try {
const python = process.platform === 'win32' ? 'py' : 'python3'
const args = process.platform === 'win32' ? ['-3'] : []
const version = execFileSync(python, [...args, '-c', 'import bridge; print(bridge.SURUM)'], {
cwd: fileURLToPath(new URL('../assets/codex-bridge/', import.meta.url)),
env: { ...process.env, AITURK_CODEX_BRIDGE_HOME: home, HERMES_HOME: home },
encoding: 'utf8',
windowsHide: true
}).trim()
expect(CODEX_BRIDGE_VERSION).toBe(version)
} finally {
rmSync(home, { recursive: true, force: true })
}
})
it('is the exact text Codex and the bridge agree on', () => {
expect(CODEX_LINK_LINE).toBe(
'openai_base_url = "http://127.0.0.1:8795/backend-api/codex" # AITURK IDE Codex Baglama'
+1 -1
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '8'
export const CODEX_BRIDGE_VERSION = '12'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
+26 -1
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
import path from 'node:path'
import { CODEX_BRIDGE_START_BUDGET_MS, type CodexLinkDeps, type CodexLinkPaths, createCodexLink } from './codex-link'
import {
@@ -21,7 +22,7 @@ const PATHS: CodexLinkPaths = {
codexConfig: 'D:\\home\\.codex\\config.toml'
}
const BRIDGE_JSON = `${PATHS.bridgeHome}\\bridge.json`
const BRIDGE_JSON = path.join(PATHS.bridgeHome, 'bridge.json')
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'
@@ -38,6 +39,7 @@ interface World {
probesUntilUp: number
autostart: null | string
regFails: boolean
configWriteFails: boolean
/** Every observable effect, in order. */
events: string[]
clock: number
@@ -58,6 +60,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
staleVersion: null,
probesUntilUp: 0,
regFails: false,
configWriteFails: false,
spawnWorks: true,
...patch
}
@@ -70,6 +73,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
exists: file => world.files.has(file),
readText: file => world.files.get(file) ?? null,
writeTextAtomic: (file, text) => {
if (file === PATHS.codexConfig && world.configWriteFails) throw new Error('config locked')
world.events.push(`write ${file}`)
world.files.set(file, text)
},
@@ -331,6 +335,27 @@ describe('applyCodexLink', () => {
expect(world.listening).toBe(false)
})
it('keeps the bridge and login item if disconnect cannot restore the config', async () => {
const { link, world } = createWorld({ listening: true })
await link.apply()
const linkedConfig = world.files.get(PATHS.codexConfig)
const autostart = world.autostart
world.events.length = 0
world.configWriteFails = true
expect(await link.apply({ enabled: false })).toMatchObject({
bridgeRunning: true, lastError: 'config-write-failed'
})
expect(world.files.get(PATHS.codexConfig)).toBe(linkedConfig)
expect(world.autostart).toBe(autostart)
expect(world.events).not.toContain('stop')
expect(world.events).not.toContain('autostart clear')
world.configWriteFails = false
expect(await link.apply({ enabled: false })).toMatchObject({ bridgeRunning: false, lastError: null })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
})
it('stays off across applies and comes back when turned on', async () => {
const { link, world } = createWorld()
world.files.set(PATHS.settingsFile, '{"enabled": false, "kapsam": "ide", "not": "ours"}')
+5 -3
View File
@@ -218,10 +218,12 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
}
}
// Off: take back everything we put there. Each step is independent — a
// failure in one must not leave the others in place.
// Remove the owned URL before stopping its server. A failed config write
// must leave the working bridge alive so Codex is not stranded offline.
const tearDown = async (config: ReturnType<typeof readCodexConfig>): Promise<BridgeProbe> => {
removeOurLine(config.text)
if (!removeOurLine(config.text)) {
return probe()
}
try {
await deps.clearAutostart()
+44
View File
@@ -378,6 +378,7 @@ import { ensureLoginShellPath } from './shell-path'
import { createBootstrapCoordinator, sshConfigFingerprint } from './ssh-bootstrap-coordinator'
import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
import { createStagedTransitionStore } from './staged-transition'
import { createStreamThrottle } from './stream-throttle'
import { registerTerminalIpc } from './terminal-ipc'
import { nativeOverlayWidth as computeNativeOverlayWidth, macTitleBarOverlayHeight } from './titlebar-overlay-width'
@@ -17192,6 +17193,21 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
// resulting state and asks for changes over IPC. Windows only (the login item is
// a Run-key value). All decisions live in electron/codex-link.ts.
const AITURK_IDE_DATA_ROOT = path.dirname(HERMES_HOME)
const stagedTransition = createStagedTransitionStore(
path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'),
path.join(app.getPath('home'), '.typesafe', 'typesafe.env'),
value =>
execFileSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
'Add-Type -AssemblyName System.Security; $bytes=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($bytes,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
],
{ input: value, encoding: 'utf8', windowsHide: true, timeout: 15000 }
).trim()
)
const codexLink = createCodexLink(
createNodeCodexLinkDeps(
@@ -17243,6 +17259,34 @@ ipcMain.handle('aiturk:codex-link:get', async event => {
return codexLink.getStatus()
})
async function stagedTransitionState() {
const state = stagedTransition.get()
try {
const response = await fetch('http://127.0.0.1:8795/aiturk/codex-baglama/status', {
signal: AbortSignal.timeout(5000)
})
if (!response.ok) throw new Error('Bridge unavailable')
const body = await response.json()
const models = Array.isArray(body.jev_models)
? body.jev_models.filter(m => typeof m?.id === 'string' && typeof m?.name === 'string')
: []
return { ...state, models, modelsAvailable: Array.isArray(body.jev_models) }
} catch {
return { ...state, models: [], modelsAvailable: false }
}
}
ipcMain.handle('aiturk:staged-transition:get', async event => {
assertCodexLinkSender(event)
return stagedTransitionState()
})
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
assertCodexLinkSender(event)
stagedTransition.save(patch)
return stagedTransitionState()
})
ipcMain.handle('aiturk:codex-link:set', async (event, patch) => {
assertCodexLinkSender(event)
+2
View File
@@ -295,6 +295,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
...(process.platform === 'win32'
? {
codexLink: {
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),
set: patch => ipcRenderer.invoke('aiturk:codex-link:set', patch)
}
@@ -0,0 +1,27 @@
export interface StagedTransitionSettings {
enabled: boolean
endpoint: string
model: string
fallbackModel: string
rejectionRules: { from: string; to: string }[]
exemptModels: string[]
thresholds: { ret: number; sart: number }
questions: { ret: string; sart: string }
criteria: { ret: { true: string; false: string }; sart: { true: string; false: string } }
timeoutSeconds: number
maxConcurrent: number
queueWaitSeconds: number
failureAction: 'pass' | 'next'
}
export interface StagedTransitionState {
settings: StagedTransitionSettings
keyConfigured: boolean
models?: { id: string; name: string }[]
modelsAvailable?: boolean
}
export interface StagedTransitionPatch {
settings: StagedTransitionSettings
apiKey?: string
}
@@ -0,0 +1,53 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createStagedTransitionStore, stagedTransitionDefaults, validateStagedTransition } from './staged-transition'
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true })
}
})
describe('staged transition settings', () => {
it('persists rules and protects keys without returning a secret', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), () => 'encrypted-test-value')
const settings = structuredClone(stagedTransitionDefaults)
settings.fallbackModel = 'fallback'
settings.rejectionRules = [{ from: 'model-a', to: 'model-b' }]
const saved = store.save({ settings, apiKey: 'dummy-secret' })
expect(saved.settings.rejectionRules).toEqual(settings.rejectionRules)
expect(saved.keyConfigured).toBe(true)
expect(JSON.stringify(saved)).not.toContain('dummy-secret')
expect(fs.readFileSync(path.join(dir, 'jev-settings.json'), 'utf8')).not.toContain('dummy-secret')
expect(store.get()).toEqual(saved)
store.save({ settings: { ...settings, timeoutSeconds: 123 } })
expect(store.get().keyConfigured).toBe(true)
})
it('rejects invalid rules without replacing the saved settings', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), x => x)
const before = store.save({ settings: structuredClone(stagedTransitionDefaults) })
expect(() =>
store.save({
settings: {
...before.settings,
rejectionRules: [
{ from: 'a', to: 'b' },
{ from: 'b', to: 'a' }
]
}
})
).toThrow()
expect(store.get().settings).toEqual(before.settings)
expect(() => validateStagedTransition({ ...before.settings, maxConcurrent: -1 })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, thresholds: { ret: NaN, sart: 0.7 } })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, endpoint: 'http://example.com' })).toThrow()
})
})
+174
View File
@@ -0,0 +1,174 @@
import fs from 'node:fs'
import path from 'node:path'
import defaults from '../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionPatch, StagedTransitionSettings, StagedTransitionState } from './staged-transition-types'
export const stagedTransitionDefaults = defaults as StagedTransitionSettings
export function validateStagedTransition(value: unknown): StagedTransitionSettings {
if (!value || typeof value !== 'object') {
throw new Error('Invalid settings')
}
const v = value as StagedTransitionSettings
const text = (s: unknown, max = 10000): s is string => typeof s === 'string' && s.trim().length > 0 && s.length <= max
const list = (s: unknown, allowEmpty: boolean) =>
Array.isArray(s) &&
(allowEmpty || s.length > 0) &&
s.length <= 64 &&
s.every(x => text(x, 256)) &&
new Set(s.map(x => x.trim())).size === s.length
const seconds = (n: unknown) => typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 3600
if (typeof v.enabled !== 'boolean' || !text(v.endpoint, 2048) || !text(v.model, 256)) {
throw new Error('Invalid connection settings')
}
const url = new URL(v.endpoint)
if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
throw new Error('HTTPS endpoint required')
}
if (
!text(v.fallbackModel, 256) ||
!list(v.exemptModels, true) ||
!Array.isArray(v.rejectionRules) ||
v.rejectionRules.length > 64 ||
v.rejectionRules.some(r => !text(r.from, 256) || !text(r.to, 256) || r.from.trim() === r.to.trim()) ||
new Set(v.rejectionRules.map(r => r.from.trim())).size !== v.rejectionRules.length
) {
throw new Error('Invalid model rules')
}
const routes = new Map(v.rejectionRules.map(r => [r.from.trim(), r.to.trim()]))
for (const start of routes.keys()) {
const seen = new Set<string>()
let current: string | undefined = start
while (current && !v.exemptModels.includes(current)) {
if (seen.has(current)) {
throw new Error('Model rules contain a cycle')
}
seen.add(current)
current = routes.get(current) ?? (current === v.fallbackModel.trim() ? undefined : v.fallbackModel.trim())
}
}
for (const id of ['ret', 'sart'] as const) {
if (
!text(v.questions?.[id]) ||
typeof v.thresholds?.[id] !== 'number' ||
!Number.isFinite(v.thresholds[id]) ||
v.thresholds[id] < 0 ||
v.thresholds[id] > 1
) {
throw new Error('Invalid evaluation rule')
}
for (const outcome of ['true', 'false'] as const) {
if (typeof v.criteria?.[id]?.[outcome] !== 'string' || v.criteria[id][outcome].length > 10000) {
throw new Error('Invalid decision criteria')
}
}
}
if (
!seconds(v.timeoutSeconds) ||
!seconds(v.queueWaitSeconds) ||
!Number.isInteger(v.maxConcurrent) ||
v.maxConcurrent < 0 ||
v.maxConcurrent > 512 ||
!['pass', 'next'].includes(v.failureAction)
) {
throw new Error('Invalid request limits')
}
return {
enabled: v.enabled,
endpoint: v.endpoint.trim(),
model: v.model.trim(),
fallbackModel: v.fallbackModel.trim(),
rejectionRules: v.rejectionRules.map(r => ({ from: r.from.trim(), to: r.to.trim() })),
exemptModels: v.exemptModels.map(x => x.trim()),
questions: { ...v.questions },
thresholds: { ...v.thresholds },
criteria: structuredClone(v.criteria),
timeoutSeconds: v.timeoutSeconds,
maxConcurrent: v.maxConcurrent,
queueWaitSeconds: v.queueWaitSeconds,
failureAction: v.failureAction
}
}
export function createStagedTransitionStore(
directory: string,
legacyKeyFile: string,
protect: (value: string) => string
) {
const file = path.join(directory, 'jev-settings.json')
const read = (): { settings: StagedTransitionSettings; protectedKey?: string } => {
if (!fs.existsSync(file)) {
return { settings: structuredClone(stagedTransitionDefaults) }
}
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
return { settings: validateStagedTransition(data.settings), protectedKey: data.protectedKey }
}
const state = (): StagedTransitionState => {
const current = read()
let legacy = Boolean(process.env.TYPESAFE_API_KEY?.trim())
if (!legacy && fs.existsSync(legacyKeyFile)) {
const match = /^\s*TYPESAFE_API_KEY\s*=\s*(.*)$/m.exec(fs.readFileSync(legacyKeyFile, 'utf8'))
legacy = Boolean(
match?.[1]
?.trim()
.replace(/^(['"])(.*)\1$/, '$2')
.trim()
)
}
return { settings: current.settings, keyConfigured: Boolean(current.protectedKey) || legacy }
}
return {
get: state,
save(patch: StagedTransitionPatch): StagedTransitionState {
const settings = validateStagedTransition(patch?.settings)
if (patch.apiKey !== undefined && (typeof patch.apiKey !== 'string' || patch.apiKey.length > 8192)) {
throw new Error('Invalid API key')
}
const current = read()
const protectedKey =
patch.apiKey === undefined
? current.protectedKey
: patch.apiKey.trim()
? protect(patch.apiKey.trim())
: undefined
fs.mkdirSync(directory, { recursive: true })
const tmp = `${file}.${process.pid}.tmp`
try {
fs.writeFileSync(tmp, JSON.stringify({ settings, protectedKey }, null, 2), { mode: 0o600 })
fs.renameSync(tmp, file)
} finally {
if (fs.existsSync(tmp)) {
fs.unlinkSync(tmp)
}
}
return state()
}
}
}
+4 -2
View File
@@ -2,12 +2,12 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.27",
"version": "1.0.0-beta.31",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
"type": "git",
"url": "git+https://gitea.twinpay.one/yilsem/aiturk-hermes-ide.git"
"url": "git+https://gitea.turkservis.online/yilsem/aiturk-hermes-ide.git"
},
"type": "module",
"main": "dist/electron-main.mjs",
@@ -304,8 +304,10 @@
"from": "assets/codex-bridge",
"to": "codex-bridge",
"filter": [
"jev-settings-defaults.json",
"**/*.py",
"**/*.pyd",
"native-watch.exe",
"**/LICENSE",
"!test_*.py"
]
+2
View File
@@ -57,6 +57,7 @@
* - electronPlatformName: 'win32' | 'darwin' | 'linux'
* - arch: Arch enum (0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal)
*/
import { buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
import { existsSync, rmSync, renameSync } from 'node:fs'
import path from 'node:path'
import { Arch } from 'electron-builder'
@@ -111,6 +112,7 @@ export function preserveRollbackBackup(appOutDir, productExeName = 'Hermes.exe')
}
export default async function beforePack(context) {
if (context?.electronPlatformName === 'win32') buildNativeWatchLauncher()
const appOutDir = context && context.appOutDir
const platformName = context && context.electronPlatformName
try {
@@ -0,0 +1,30 @@
/** Build the Windows artifact from source on Windows or the Linux/Wine CI host. */
import { execFileSync } from 'node:child_process'
import { rmSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const sourceHome = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets/codex-bridge')
export function compilerCommand({ platform = process.platform, env = process.env, source, output }) {
const args = ['/nologo', '/target:exe', '/platform:anycpu', '/reference:System.Web.Extensions.dll', `/out:${output}`, source]
if (platform === 'win32') {
return { command: path.win32.join(env.SystemRoot || 'C:/Windows', 'Microsoft.NET/Framework64/v4.0.30319/csc.exe'), args }
}
if (platform === 'linux') return { command: 'mcs', args }
throw new Error(`Windows launcher compilation is not configured on ${platform}`)
}
export function buildNativeWatchLauncher({ home = sourceHome, ...options } = {}) {
const output = path.join(home, 'native-watch.exe')
const invocation = compilerCommand({ ...options, source: path.join(home, 'NativeWatchBootstrap.cs'), output })
// A missing compiler or broken source must fail packaging, never reuse an old EXE.
rmSync(output, { force: true })
execFileSync(invocation.command, invocation.args, { windowsHide: true, stdio: 'inherit' })
const bytes = readFileSync(output)
if (bytes.length < 64 || bytes.toString('ascii', 0, 2) !== 'MZ' || bytes.toString('ascii', bytes.readUInt32LE(60), bytes.readUInt32LE(60) + 4) !== 'PE\0\0') {
rmSync(output, { force: true })
throw new Error('Native watch compiler did not produce a Windows PE executable')
}
return output
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) buildNativeWatchLauncher()
@@ -0,0 +1,36 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { compilerCommand, buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
const desktop = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
test('Windows and Linux hosts build the same Windows source with the required assembly', () => {
const input = { source: 'source with spaces.cs', output: 'output with spaces.exe' }
const win = compilerCommand({ ...input, platform: 'win32', env: { SystemRoot: 'D:/Windows' } })
const linux = compilerCommand({ ...input, platform: 'linux' })
assert.equal(win.command, 'D:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe')
assert.equal(linux.command, 'mcs')
assert.deepEqual(win.args, linux.args)
assert.ok(linux.args.includes('/reference:System.Web.Extensions.dll'))
assert.equal(linux.args.at(-1), input.source)
assert.throws(() => compilerCommand({ ...input, platform: 'darwin' }), /not configured/)
})
test('real host compiler builds PE from source, and broken source fails without stale artifact', () => {
// Inside the source workspace, including on the mounted Linux CI fixture.
const home = mkdtempSync(path.join(desktop, '.native-watch-test-'))
try {
copyFileSync(path.join(desktop, 'assets/codex-bridge/NativeWatchBootstrap.cs'), path.join(home, 'NativeWatchBootstrap.cs'))
const exe = buildNativeWatchLauncher({ home })
const first = readFileSync(exe)
assert.equal(first.toString('ascii', 0, 2), 'MZ')
writeFileSync(exe, 'stale artifact')
buildNativeWatchLauncher({ home })
assert.equal(readFileSync(exe).toString('ascii', 0, 2), 'MZ')
writeFileSync(path.join(home, 'NativeWatchBootstrap.cs'), 'invalid C# source')
assert.throws(() => buildNativeWatchLauncher({ home }))
assert.equal(existsSync(exe), false)
} finally { rmSync(home, { recursive: true, force: true }) }
})
+13 -2
View File
@@ -36,7 +36,18 @@ function electronBuilderCli() {
return path.join(path.dirname(pkgJson), rel)
}
const dist = electronDistDir()
const cliArgs = process.argv.slice(2)
// An installed Electron dist belongs to the host. Cross-platform packaging
// must download the requested target instead of copying the host executable.
const crossPlatform =
(cliArgs.includes('--win') && process.platform !== 'win32') ||
(cliArgs.includes('--mac') && process.platform !== 'darwin') ||
(cliArgs.includes('--linux') && process.platform !== 'linux')
const crossArch =
(cliArgs.includes('--x64') && process.arch !== 'x64') ||
(cliArgs.includes('--arm64') && process.arch !== 'arm64') ||
(cliArgs.includes('--ia32') && process.arch !== 'ia32')
const dist = crossPlatform || crossArch ? null : electronDistDir()
// Local `hermes desktop` builds only ever package (--dir or dist), never
// publish a GitHub release — no CI workflow drives this script. But the npm
// lifecycle env sets CI=1 (so esbuild's postinstall doesn't try interactive
@@ -55,7 +66,7 @@ if (dist && fs.existsSync(distBinary(dist))) {
"via @electron/get (electronVersion + ELECTRON_MIRROR)."
)
}
args.push(...process.argv.slice(2))
args.push(...cliArgs)
const result = spawnSync(process.execPath, [electronBuilderCli(), ...args], {
stdio: "inherit",
+10 -6
View File
@@ -546,7 +546,7 @@ export function stageGetWindowsInto(
export function installGetWindowsNativeBinding(
srcRoot,
{ resolveInstaller, spawn = spawnSync } = {}
{ resolveInstaller, spawn = spawnSync, platform = process.platform, arch = process.arch } = {}
) {
let installerPath
try {
@@ -562,7 +562,11 @@ export function installGetWindowsNativeBinding(
throw new Error(`[stage-native-deps] cannot resolve get-windows native installer: ${detail}`)
}
const result = spawn(process.execPath, [installerPath, 'install', '--fallback-to-build'], {
const crossTarget = platform !== process.platform || arch !== process.arch
const installerArgs = crossTarget
? ['install', `--target_platform=${platform}`, `--target_arch=${arch}`, ...(platform === 'win32' ? ['--target_libc=unknown'] : []), '--fallback-to-build=false']
: ['install', '--fallback-to-build']
const result = spawn(process.execPath, [installerPath, ...installerArgs], {
cwd: srcRoot,
stdio: 'inherit'
})
@@ -606,11 +610,11 @@ export function stageGetWindows(
)
}
// Only a win32 host can produce the win32 binding, so a cross-platform pack
// has nothing to gain from the native installer.
// Download the target prebuild even on Linux/Wine; cross-target installs
// never fall back to compiling a host binary.
const install =
platform === 'win32' && process.platform === 'win32'
? () => installGetWindowsNativeBinding(srcRoot)
platform === 'win32' && arch !== 'arm64'
? () => installGetWindowsNativeBinding(srcRoot, { platform, arch })
: undefined
return stageGetWindowsInto(srcRoot, destRoot, { platform, arch, install })
}
@@ -594,7 +594,9 @@ test('darwin staging ships the Swift helper executable and the rewritten windows
stageGetWindowsInto(srcRoot, destRoot, { platform: 'darwin' })
assert.equal(fs.statSync(join(destRoot, 'main')).mode & 0o777, 0o755)
if (process.platform !== 'win32') {
assert.equal(fs.statSync(join(destRoot, 'main')).mode & 0o777, 0o755)
}
const staged = fs.readFileSync(join(destRoot, 'lib', 'windows.js'), 'utf8')
assert.match(staged, /Rewritten by stage-native-deps\.mjs/)
assert.ok(!staged.includes('node-pre-gyp'), 'pre-gyp loader must not survive staging')
@@ -635,3 +637,16 @@ test('win32-x64 staging fails when get-windows is absent', () => {
/get-windows is not installed/
)
})
// Linux/Wine packaging must fetch the Windows payload, never compile for Linux.
test('get-windows cross-target install requests the target prebuild without compilation fallback', () => {
const calls = []
const platform = process.platform === 'win32' ? 'linux' : 'win32'
installGetWindowsNativeBinding('/fake/get-windows', {
platform, arch: 'x64', resolveInstaller: () => '/fake/node-pre-gyp',
spawn: (command, args) => { calls.push(args); return { status: 0 } }
})
assert.deepEqual(calls[0], ['/fake/node-pre-gyp', 'install',
`--target_platform=${platform}`, '--target_arch=x64',
...(platform === 'win32' ? ['--target_libc=unknown'] : []), '--fallback-to-build=false'])
})
@@ -22,7 +22,7 @@ import {
import { ListRow, SectionHeading, SettingsContent } from './primitives'
import { UninstallSection } from './uninstall-section'
const RELEASE_NOTES_URL = 'https://gitea.twinpay.one/yilsem/aiturk-hermes-ide/releases'
const RELEASE_NOTES_URL = 'https://gitea.turkservis.online/yilsem/aiturk-hermes-ide/releases'
const INSTALLER_URL = 'https://turkservis.online/ide'
function relativeTime(ms: number | undefined, a: Translations['settings']['about']) {
@@ -106,7 +106,7 @@ export function AboutSettings() {
<p className="text-sm text-muted-foreground">{copy.about}</p>
<div className="flex flex-wrap gap-4 text-sm">
<a className="text-primary underline" href={INSTALLER_URL} rel="noreferrer" target="_blank">{copy.downloads}</a>
<a className="text-primary underline" href="https://gitea.twinpay.one/yilsem/aiturk-hermes-ide" rel="noreferrer" target="_blank">{copy.source}</a>
<a className="text-primary underline" href="https://gitea.turkservis.online/yilsem/aiturk-hermes-ide" rel="noreferrer" target="_blank">{copy.source}</a>
</div>
</section>
<div className="flex flex-col items-center gap-3 pt-6 pb-2 text-center">
@@ -139,6 +139,13 @@ describe('Codex Link settings', () => {
expect(screen.getByRole('switch', { name: 'Codex Bağlama' }).getAttribute('aria-checked')).toBe('false')
})
it('does not claim original settings were restored when disconnect failed', async () => {
get.mockResolvedValue({ ...RUNNING, enabled: false, lastError: 'config-write-failed' })
renderPage()
expect(await screen.findByText('Codex ayar dosyası yazılamadı.')).toBeTruthy()
expect(screen.queryByText('Codex Bağlama kapalı. Codex kendi ayarlarıyla çalışıyor.')).toBeNull()
})
it('rolls the switch back and reports when the change fails', async () => {
get.mockResolvedValue(RUNNING)
set.mockRejectedValue(new Error('codex-link:unavailable'))
@@ -40,6 +40,7 @@ export function codexLinkStatusLines(status: CodexLinkStatus, copy: CodexLinkCop
}
if (!status.enabled) {
if (status.lastError) return [...lines, ...error]
return [...lines, { id: 'off', text: copy.off, tone: 'muted' }, ...error]
}
+27 -3
View File
@@ -58,6 +58,7 @@ import { NotificationsSettings } from './notifications-settings'
import { PluginsSettings } from './plugins-settings'
import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings'
import { SessionsSettings } from './sessions-settings'
import { StagedTransitionSettingsView } from './staged-transition-settings'
import type { SettingsPageProps, SettingsView as SettingsViewId } from './types'
const SETTINGS_VIEWS: readonly SettingsViewId[] = [
@@ -72,13 +73,14 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
'notifications',
'billing',
'codex-link',
'staged-transition',
'plugins',
'sessions',
'about'
]
export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) {
const { t } = useI18n()
const { t, locale } = useI18n()
const aiturkCopy = useAiturkCopy()
const codexLinkCopy = useCodexLinkCopy()
const navigate = useNavigate()
@@ -267,7 +269,9 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'aiturk-phone',
label: aiturkCopy.mobile.nav,
onSelect: () => {
void window.hermesDesktop?.aiturkMobile?.open().catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
void window.hermesDesktop?.aiturkMobile
?.open()
.catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
}
}
]
@@ -282,6 +286,13 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'codex-link',
label: codexLinkCopy.nav,
onSelect: () => setActiveView('codex-link')
},
{
active: activeView === 'staged-transition',
icon: Link,
id: 'staged-transition',
label: locale === 'tr' ? 'Kademeli Geçiş' : 'Staged Transition',
onSelect: () => setActiveView('staged-transition')
}
]
: []),
@@ -338,7 +349,18 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
onSelect: () => setActiveView('about')
}
],
[activeView, keysView, providerView, t, aiturkCopy, codexLinkCopy, setActiveView, openProviderView, openKeysView]
[
activeView,
keysView,
providerView,
t,
locale,
aiturkCopy,
codexLinkCopy,
setActiveView,
openProviderView,
openKeysView
]
)
// Type-to-search: printable keystrokes on the Settings surface (outside any
@@ -459,6 +481,8 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
<PluginsSettings />
) : activeView === 'codex-link' ? (
<CodexLinkSettings />
) : activeView === 'staged-transition' ? (
<StagedTransitionSettingsView />
) : (
<SessionsSettings />
)
@@ -0,0 +1,71 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import defaults from '../../../assets/codex-bridge/jev-settings-defaults.json'
import { StagedTransitionSettingsView } from './staged-transition-settings'
const get = vi.fn()
const set = vi.fn()
const state = () => ({
settings: structuredClone(defaults),
keyConfigured: true,
modelsAvailable: true,
models: [
{ id: 'A', name: 'Model A' },
{ id: 'B', name: 'Model B' }
]
})
beforeEach(() => {
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getStagedTransition: get, setStagedTransition: set }
}
get.mockResolvedValue(state())
set.mockImplementation(async patch => ({ ...state(), settings: patch.settings }))
})
afterEach(() => {
cleanup()
vi.resetAllMocks()
})
const show = () =>
render(
<I18nProvider configClient={null} initialLocale="tr">
<StagedTransitionSettingsView />
</I18nProvider>
)
describe('staged transition settings UI', () => {
it('adds and removes model routing and always-pass rows', async () => {
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.click(screen.getByText('+ Geçiş kuralı ekle'))
expect(screen.getByLabelText('İtiraz edilen model 4')).toBeTruthy()
expect(screen.getByLabelText('Kullanılacak model 4')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Geçiş kuralı 4 sil'))
expect(screen.queryByLabelText('İtiraz edilen model 4')).toBeNull()
fireEvent.click(screen.getByText('+ Her zaman geç kuralı ekle'))
expect(screen.getByLabelText('Her zaman geçen model 3')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Her zaman geç kuralı 3 sil'))
fireEvent.change(screen.getByLabelText('İsteği yerine getirmeme denetim metni'), {
target: { value: 'custom prompt' }
})
fireEvent.click(screen.getByText('Kaydet'))
await screen.findByText('Kaydedildi. Yeni değerlendirmelerde uygulanır.')
expect(set.mock.calls[0][0].settings.questions.ret).toBe('custom prompt')
expect(set.mock.calls[0][0]).not.toHaveProperty('apiKey')
})
it('retains user edits if persistence fails', async () => {
set.mockRejectedValue(new Error('disk error'))
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.change(screen.getByLabelText('Jev modeli'), { target: { value: 'new-model' } })
fireEvent.click(screen.getByText('Kaydet'))
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('Kaydedilemedi'))
expect((screen.getByLabelText('Jev modeli') as HTMLInputElement).value).toBe('new-model')
})
})
@@ -0,0 +1,463 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import defaultSettings from '../../../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionSettings, StagedTransitionState } from '../../../electron/staged-transition-types'
import { ListRow, SettingsContent, SettingsSection } from './primitives'
export function StagedTransitionSettingsView() {
const { locale } = useI18n()
const tr = locale === 'tr'
const text = (turkish: string, english: string) => (tr ? turkish : english)
const api = window.hermesDesktop?.codexLink
const [state, setState] = useState<StagedTransitionState | null>(null)
const [draft, setDraft] = useState<StagedTransitionSettings | null>(null)
const [key, setKey] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState(false)
const adopt = (next: StagedTransitionState) => {
setState(next)
setDraft(next.settings)
}
useEffect(() => {
let active = true
if (api?.getStagedTransition) {
api.getStagedTransition().then(
next => {
if (active) {
adopt(next)
}
},
() => {
if (active) {
setError(true)
setMessage(tr ? 'Ayarlar okunamadı.' : 'Could not read settings.')
}
}
)
}
return () => {
active = false
}
}, [api, tr])
if (!api?.getStagedTransition) {
return (
<SettingsContent>
<p>
{text('Bu bölüm Windows masaüstü uygulamasında kullanılabilir.', 'Available in the Windows desktop app.')}
</p>
</SettingsContent>
)
}
if (!draft) {
return (
<SettingsContent>
<p role={error ? 'alert' : 'status'}>{message || text('Yükleniyor…', 'Loading…')}</p>
</SettingsContent>
)
}
const change = <K extends keyof StagedTransitionSettings>(name: K, value: StagedTransitionSettings[K]) =>
setDraft({ ...draft, [name]: value })
const models = new Map((state?.models ?? []).map(model => [model.id, model.name]))
const storedModels = [
draft.fallbackModel,
...draft.exemptModels,
...draft.rejectionRules.flatMap(rule => [rule.from, rule.to])
]
for (const id of storedModels) {
if (id && !models.has(id)) {
models.set(id, `${id} (${text('kayıtlı', 'saved')})`)
}
}
const modelSelect = (label: string, value: string, onChange: (value: string) => void) => (
<div className="w-full min-w-0 sm:w-auto sm:min-w-48 sm:flex-1">
<Select onValueChange={onChange} value={value}>
<SelectTrigger aria-label={label}>
<SelectValue placeholder={text('Model seçin', 'Select a model')} />
</SelectTrigger>
<SelectContent>
{Array.from(models, ([id, name]) => (
<SelectItem key={id} value={id}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
const number = (
name: 'timeoutSeconds' | 'maxConcurrent' | 'queueWaitSeconds',
label: string,
min: number,
max: number
) => (
<ListRow
action={
<Input
aria-label={label}
max={max}
min={min}
onChange={event => change(name, Number(event.target.value))}
type="number"
value={draft[name]}
/>
}
title={label}
/>
)
const save = async () => {
setBusy(true)
setMessage('')
setError(false)
try {
adopt(await api.setStagedTransition({ settings: draft, ...(key.trim() ? { apiKey: key.trim() } : {}) }))
setKey('')
setMessage(text('Kaydedildi. Yeni değerlendirmelerde uygulanır.', 'Saved. Applies to new evaluations.'))
} catch (failure) {
setError(true)
const cycle = String(failure).includes('cycle')
setMessage(
cycle
? text(
'Geçiş kuralları döngü oluşturuyor. Aynı modele geri dönen geçişi değiştirin.',
'Transition rules form a cycle. Change the rule that returns to an earlier model.'
)
: text(
'Kaydedilemedi. Alanları ve sayı aralıklarını kontrol edin; önceki ayarlar korunuyor.',
'Could not save. Check the fields and number ranges; previous settings are preserved.'
)
)
} finally {
setBusy(false)
}
}
return (
<SettingsContent>
<fieldset className="min-w-0" disabled={busy}>
<SettingsSection icon={Link} title={text('Kademeli Geçiş', 'Staged Transition')}>
<ListRow
action={
<Switch
aria-label={text('Jev denetimi', 'Jev evaluation')}
checked={draft.enabled}
onCheckedChange={value => change('enabled', value)}
/>
}
description={text(
'Köprüdeki ek modellerin nihai cevaplarını denetler. Uygun bulunmayan cevap sıradaki katmanda yeniden hazırlanır.',
'Evaluates final answers from bridge models. Rejected answers are retried with the next layer.'
)}
title={text('Jev denetimi ve kademeli geçiş', 'Jev evaluation and staged transition')}
/>
<ListRow
action={
<Input
aria-label={text('Jev modeli', 'Jev model')}
onChange={e => change('model', e.target.value)}
value={draft.model}
/>
}
title={text('Jev modeli', 'Jev model')}
/>
<ListRow
action={
<Input
aria-label={text('API adresi', 'API endpoint')}
onChange={e => change('endpoint', e.target.value)}
type="url"
value={draft.endpoint}
/>
}
title={text('API adresi', 'API endpoint')}
/>
<ListRow
action={
<Input
aria-label={text('TypeSafe API anahtarı', 'TypeSafe API key')}
autoComplete="new-password"
onChange={e => setKey(e.target.value)}
type="password"
value={key}
/>
}
description={
state?.keyConfigured
? text(
'Anahtar tanımlı. Değiştirmek için yeni anahtarı girin.',
'Key configured. Enter a new key to replace it.'
)
: text('Henüz anahtar tanımlı değil.', 'No key configured.')
}
title={text('TypeSafe API anahtarı', 'TypeSafe API key')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Denetim kuralları', 'Evaluation rules')}>
{(['ret', 'sart'] as const).map(id => {
const label =
id === 'ret'
? text('İsteği yerine getirmeme', 'Request not fulfilled')
: text('İsteği kendiliğinden değiştirme', 'Unrequested changes')
return (
<div key={id}>
<ListRow
action={
<Input
aria-label={`${label} ${text('eşiği', 'threshold')}`}
max={1}
min={0}
onChange={e => change('thresholds', { ...draft.thresholds, [id]: Number(e.target.value) })}
step={0.01}
type="number"
value={draft.thresholds[id]}
/>
}
description={text(
'0–1 arasında geçiş eşiği. Olasılık bu değere ulaşırsa sıradaki katman denenir.',
'Transition threshold from 0 to 1. A probability at or above this value tries the next layer.'
)}
title={label}
/>
<Textarea
aria-label={`${label} ${text('denetim metni', 'instructions')}`}
onChange={e => change('questions', { ...draft.questions, [id]: e.target.value })}
rows={4}
value={draft.questions[id]}
/>
{(['true', 'false'] as const).map(outcome => (
<label className="block py-2" key={outcome}>
<span>
{outcome === 'true'
? text(
'“Evet, sorun var” ne demek? (isteğe bağlı)',
'What does “yes, there is a problem” mean? (optional)'
)
: text(
'“Hayır, sorun yok” ne demek? (isteğe bağlı)',
'What does “no problem” mean? (optional)'
)}
</span>
<Textarea
aria-label={`${label} ${outcome}`}
onChange={e =>
change('criteria', {
...draft.criteria,
[id]: { ...draft.criteria[id], [outcome]: e.target.value }
})
}
rows={2}
value={draft.criteria[id][outcome]}
/>
</label>
))}
</div>
)
})}
</SettingsSection>
<SettingsSection
icon={Link}
title={text('Olasılık, onay ve sıcaklık', 'Probability, approval and temperature')}
>
<p>
{text(
'Jev her denetim sorusu için 0–1 arasında sorun olasılığı verir. Her iki olasılık da kendi itiraz eşiğinin altındaysa cevap geçer. Örneğin 0,70 eşiği, sorun olasılığı %70 veya üstündeyse geçiş demektir; %70 kalite garantisi değildir.',
'Jev returns a problem probability from 0 to 1 for each question. An answer passes when both probabilities are below their rejection thresholds. A threshold of 0.70 triggers transition at 70% problem probability; it is not a quality guarantee.'
)}
</p>
<ListRow
description={text('TypeSafe API bu parametreyi sunmuyor.', 'Not exposed by the TypeSafe API.')}
title={text('Sıcaklık', 'Temperature')}
/>
<ListRow
description={text(
'Kullanılan Noul (evet/hayır) türünde ayrı güven skoru yoktur. Karar yukarıdaki olasılık eşikleriyle verilir.',
'The Noul (yes/no) type has no separate confidence score. Decisions use the probability thresholds above.'
)}
title={text('Ayrı emin olma / güven değeri', 'Separate confidence value')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Geçiş kuralları', 'Transition rules')}>
{!state?.modelsAvailable && (
<p role="status">
{text(
'Canlı model listesi alınamadı. Kayıtlı seçimler korunuyor; Codex Bağlama açıkken listeyi yenileyin.',
'Live model list unavailable. Saved selections are preserved; refresh with Codex Link enabled.'
)}
</p>
)}
<Button
onClick={async () => {
try {
const next = await api.getStagedTransition()
setState(next)
} catch {
setError(true)
setMessage(text('Model listesi alınamadı.', 'Could not load models.'))
}
}}
variant="outline"
>
{text('Model listesini yenile', 'Refresh models')}
</Button>
<ListRow
action={modelSelect(text('Genel geçiş modeli', 'Default fallback model'), draft.fallbackModel, value =>
change('fallbackModel', value)
)}
description={text(
'Modele özel kural yoksa seçilen model kullanılır.',
'Uses this model when there is no model-specific rule.'
)}
title={text('Jev herhangi bir modele itiraz ederse', 'When Jev rejects any model')}
/>
{draft.rejectionRules.map((rule, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`İtiraz edilen model ${index + 1}`, `Rejected model ${index + 1}`), rule.from, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, from: value } : r))
)
)}
<span>{text('modeline Jev itiraz ederse', 'rejected by Jev →')}</span>
{modelSelect(text(`Kullanılacak model ${index + 1}`, `Fallback model ${index + 1}`), rule.to, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, to: value } : r))
)
)}
<span>{text('modeli kullanılsın', 'use this model')}</span>
<Button
aria-label={text(`Geçiş kuralı ${index + 1} sil`, `Remove rule ${index + 1}`)}
onClick={() =>
change(
'rejectionRules',
draft.rejectionRules.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button
onClick={() => change('rejectionRules', [...draft.rejectionRules, { from: '', to: '' }])}
variant="outline"
>
{text('+ Geçiş kuralı ekle', '+ Add transition rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Jev’den her zaman geçsin', 'Always pass Jev')}>
<p>
{text(
'Bu modeller Jev denetimine gönderilmez. Bu seçim, modele özel ve genel itiraz kurallarından önce uygulanır.',
'These models bypass Jev evaluation. This takes priority over model-specific and default rejection rules.'
)}
</p>
{draft.exemptModels.map((model, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`Her zaman geçen model ${index + 1}`, `Always-pass model ${index + 1}`), model, value =>
change(
'exemptModels',
draft.exemptModels.map((m, i) => (i === index ? value : m))
)
)}
<span>{text('modeli Jev’den her zaman geçsin', 'always passes Jev')}</span>
<Button
aria-label={text(`Her zaman geç kuralı ${index + 1} sil`, `Remove always-pass rule ${index + 1}`)}
onClick={() =>
change(
'exemptModels',
draft.exemptModels.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button onClick={() => change('exemptModels', [...draft.exemptModels, ''])} variant="outline">
{text('+ Her zaman geç kuralı ekle', '+ Add always-pass rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Paralellik ve bekleme', 'Concurrency and waiting')}>
{number(
'maxConcurrent',
text('Eşzamanlı Jev isteği (0: yerel sınır yok)', 'Concurrent Jev requests (0: no local limit)'),
0,
512
)}
{number(
'queueWaitSeconds',
text('Sırada en fazla bekleme (saniye)', 'Maximum queue wait (seconds)'),
1,
3600
)}
{number('timeoutSeconds', text('Jev yanıt zaman aşımı (saniye)', 'Jev response timeout (seconds)'), 1, 3600)}
<ListRow
action={
<Select
onValueChange={value => change('failureAction', value as 'pass' | 'next')}
value={draft.failureAction}
>
<SelectTrigger aria-label={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pass">{text('Mevcut cevabı ilet', 'Pass the current answer')}</SelectItem>
<SelectItem value="next">{text('Sıradaki katmanı dene', 'Try the next layer')}</SelectItem>
</SelectContent>
</Select>
}
title={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}
/>
</SettingsSection>
<Button onClick={() => void save()}>{busy ? text('Kaydediliyor…', 'Saving…') : text('Kaydet', 'Save')}</Button>
<Button
onClick={() => {
setDraft(structuredClone(defaultSettings) as StagedTransitionSettings)
setMessage(
text(
'Varsayılanlar forma yüklendi. Uygulamak için Kaydet’e basın.',
'Defaults loaded into the form. Save to apply.'
)
)
setError(false)
}}
variant="ghost"
>
{text('Varsayılanları yükle', 'Load defaults')}
</Button>
{message && (
<p className="py-3" role={error ? 'alert' : 'status'}>
{message}
</p>
)}
</fieldset>
</SettingsContent>
)
}
+1
View File
@@ -16,6 +16,7 @@ export type SettingsView =
| 'plugins'
| 'providers'
| 'sessions'
| 'staged-transition'
| `config:${string}`
export type EnvPatch = Partial<Pick<EnvVarInfo, 'is_set' | 'redacted_value'>>
+3
View File
@@ -3,6 +3,7 @@ import type { TranslucencyState } from '@hermes/shared/translucency'
import type { CodexLinkPatch, CodexLinkStatus } from '../electron/codex-link-types'
import type { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
import type { WakeIndicatorState } from './lib/wake-indicator'
import type {
@@ -340,6 +341,8 @@ declare global {
* use the IDE's models. `get` only reads; `set` applies and returns the
* state that actually resulted. Windows only. */
codexLink?: {
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>
set: (patch: CodexLinkPatch) => Promise<CodexLinkStatus>
}
+6 -1
View File
@@ -16,6 +16,11 @@
"rootDir": "..",
"outDir": "build/electron-types"
},
"include": ["electron", "../shared/src/data-url-read-max.ts", "../shared/src/translucency.ts"],
"include": [
"electron",
"assets/codex-bridge/jev-settings-defaults.json",
"../shared/src/data-url-read-max.ts",
"../shared/src/translucency.ts"
],
"exclude": ["src", "electron/**/*.e2e.mts"]
}
+1 -1
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.27",
"version": "1.0.0-beta.31",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",
+5 -5
View File
@@ -383,8 +383,8 @@ $script:ResolvedPathReport = @{
# Configuration
# ============================================================================
$RepoUrlSsh = "https://gitea.twinpay.one/yilsem/aiturk-hermes-ide.git"
$RepoUrlHttps = "https://gitea.twinpay.one/yilsem/aiturk-hermes-ide.git"
$RepoUrlSsh = "https://gitea.turkservis.online/yilsem/aiturk-hermes-ide.git"
$RepoUrlHttps = "https://gitea.turkservis.online/yilsem/aiturk-hermes-ide.git"
$PythonVersion = "3.11"
# Minor versions the installer accepts when the requested $PythonVersion isn't
# available, in preference order. Only checkout-private uv-managed interpreters
@@ -2417,13 +2417,13 @@ function Install-Repository {
# for. GitHub supports archive URLs for commits, tags, and
# branches; we honour Commit > Tag > Branch.
if ($Commit) {
$zipUrl = "https://gitea.twinpay.one/yilsem/aiturk-hermes-ide/archive/$Commit.zip"
$zipUrl = "https://gitea.turkservis.online/yilsem/aiturk-hermes-ide/archive/$Commit.zip"
$zipLabel = $Commit
} elseif ($Tag) {
$zipUrl = "https://gitea.twinpay.one/yilsem/aiturk-hermes-ide/archive/$Tag.zip"
$zipUrl = "https://gitea.turkservis.online/yilsem/aiturk-hermes-ide/archive/$Tag.zip"
$zipLabel = $Tag
} else {
$zipUrl = "https://gitea.twinpay.one/yilsem/aiturk-hermes-ide/archive/$Branch.zip"
$zipUrl = "https://gitea.turkservis.online/yilsem/aiturk-hermes-ide/archive/$Branch.zip"
$zipLabel = $Branch
}
$zipPath = "$env:TEMP\hermes-agent-$zipLabel.zip"