- One decision per real turn: a turn starts with a user message after the previous final answer; tool steps, messages typed mid-turn, a lone "." and compaction keep the combo. Works without prompt_cache_key too. - The virtual Otomatik name never reaches the gateway (stage off or error -> last choice or targets.zor); prefilter.target may not be it. - finishCheck: at most once per turn; a broken, empty or refused retry delivers the already approved first answer. - Layer window filter: only trusted windows, never the last layer. - Correction channel only on always-pass targets while checks are on; Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame passes unfiltered instead of breaking the stream. - Turn-start questions wait in the local queue no longer than their timeout; decision-turn memory under a lock with refreshed order. - Dify fetch refuses redirects (the app key never moves host); the rules file write retries a Windows sharing violation. - An unreadable settings file no longer breaks every request. - TypeScript accepts only https:// URLs the bridge accepts; finishCheck.timeoutSeconds (unused) removed. Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the turn, prefilter (incl. decision timeout) and finish-check paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# AITURK IDE Codex bridge
|
||
|
||
Local HTTP proxy that AITURK IDE's **Settings → Codex Bağlama** runs for OpenAI Codex
|
||
(desktop or CLI) on the same computer. Standard-library Python only; it runs on the
|
||
agent venv's interpreter and keeps running after the IDE window closes, because
|
||
Codex depends on it while `openai_base_url` points here.
|
||
|
||
- `bridge.py`: the proxy. Subscription models go to chatgpt.com unchanged; `ide/<id>`
|
||
models go to the AITURK gateway configured in the agent's `config.yaml`, with the
|
||
key read from the agent's `.env` on every catalog refresh (never written elsewhere).
|
||
- `ide_catalog.py`: which IDE models and combos are added to Codex's model list and
|
||
how they are named (`provider/model`, combos by bare name).
|
||
The public `/models` catalog supplies every OmniRoute chat model; a member does
|
||
not need a private local model allowlist. `/router/catalog` supplies only the
|
||
`kind` metadata used for sorting, never additional model IDs. Direct 9Router
|
||
entries and internal 9Router combos are excluded. Codex's own subscription models
|
||
stay first, in their own order and under their own names; added entries follow
|
||
them: combos A–Z, then individual models by provider and name. A saved model
|
||
selection remains unchanged. A failed or empty refresh keeps the last successful catalog.
|
||
- `ozet_kopru.py`: conversation summaries are produced as plain text, so a summary made
|
||
by one provider stays readable after the user switches to another.
|
||
- `gemini_context.py`, `resim_context.py`: request fixes for routed models.
|
||
|
||
Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE writes
|
||
`<data dir>/bridge.json` and reads `GET /aiturk/codex-baglama/status`;
|
||
`POST /aiturk/codex-baglama/stop` ends the process.
|
||
|
||
Tests: `python -m unittest discover` in this directory.
|
||
|
||
## Version 19: decision stages (14 October 2026)
|
||
|
||
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
|
||
several stages. Every new stage is switched on in Settings → Kademeli Geçiş and starts disabled, so an
|
||
upgrade changes no behaviour by itself. The decision model never writes text; an unreachable or invalid
|
||
decision always keeps the current path.
|
||
|
||
- `jev-settings.json` schema 2 adds `watchModels`, `prefilter`, `autoRoute`, `finishCheck`,
|
||
`verifierCheck` and `policy`. A schema 1 file is filled from `jev-settings-defaults.json`.
|
||
The default chain is `Katman-1 → Katman-2 → Katman-3`; `Katman-3`, `Chat-Model`,
|
||
`bekci-sesli-bekci` and `Sesli-Hizli-Zeki` always pass.
|
||
- **Correction channel** (`watchModels`): the Bekçi watch sideband is no longer bound to the hard-coded
|
||
name `Sesli-Hizli-Zeki`; any exact target in the list owns it, but only while it always passes the check
|
||
(a checked answer may be replaced by a layer, and a receipt must not belong to an answer the user never saw).
|
||
A single SSE frame above 256 KB no longer breaks the stream; the rest of the stream passes unfiltered.
|
||
- **Refusal pre-check** (`prefilter`): at the start of a turn, "would a mainstream model refuse or water
|
||
this down?". If yes, the step starts at `prefilter.target` instead of collecting a refusal first.
|
||
- **Automatic model** (`autoRoute`): a virtual `ide/<model>` entry ("Otomatik") in the Codex list. A new turn
|
||
(a user message after the previous final answer) gets one decision call (hard? research?) and goes to
|
||
the configured combo. Tool steps, messages typed while the turn runs, a lone "." and compaction stay on
|
||
the same combo, because switching providers mid-turn breaks encrypted history. When the stage is off or
|
||
fails, the virtual name never reaches the gateway: the last choice or `targets.zor` is used. The user's
|
||
explicit model choice is never changed by this stage.
|
||
- Both turn-start questions go in one call; the wait, including the local queue, is bounded by
|
||
`timeoutSeconds` (default 3 s).
|
||
- **Layer window filter**: a layer whose trusted context window (pencere.bekci_calisir) cannot hold the
|
||
request is skipped. The last link of the chain is never skipped.
|
||
- **Early finish check** (`finishCheck`): asked in the same call as the refusal check on a final answer.
|
||
If the answer stopped early or claimed success without evidence, it goes back to the same model with
|
||
`finishCheck.note`, at most once per turn. If the retry breaks, comes back empty or fails the check, the
|
||
first (already approved) answer is delivered.
|
||
- **Rules from Dify** (`policy.source = dify`, `karar_politikasi.py`): the stage questions, thresholds and
|
||
combo choices may come from a Dify workflow's `kurallar` output. Fetched in the background every
|
||
`refreshMinutes`, validated against the local settings, last good copy kept in `jev-policy.json`.
|
||
The transition chain and the always-pass list stay local. A turn never waits for Dify, and a redirect
|
||
from the Dify endpoint is refused so the app key never travels to another host.
|
||
- **Workflow reviewer check** (`verifierCheck`, `karar_sor.py`): the Codex dynamic-workflow acceptance
|
||
step asks whether a reviewer approved without concrete evidence; if so acceptance is blocked.
|
||
|
||
## 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
|
||
message that STARTS a turn; a message that joins a running turn (including the lone `.`) does not carry it.
|
||
`model_gecis.py` therefore handles the change in the bridge. A short user message such as "modeli gemini flash low
|
||
yap" (spoken through the voice assistant or typed) is matched against the model list; the chat's requests are then
|
||
routed to that model (`model-yonlendirme.json`, keyed by `prompt_cache_key`) until Codex sends a different model
|
||
for the chat, i.e. until the user picks another one in the list.
|
||
|
||
- Idle chat: the bridge answers itself ("Sohbetin modeli artık X ..."), which also tells the voice assistant.
|
||
- Running turn: the command is replaced by a handover note and the SAME turn continues on the new model.
|
||
- "seçili model hangisi" / "ana model kim" is answered by the bridge with the model actually in use.
|
||
- An ambiguous name changes nothing and lists the closest names; a name matching no model is not a command.
|
||
- A lone `.` that joins a running turn is dropped instead of answered (answering it cut the work short).
|
||
- Handled commands left in the history are turned into notes so a later model does not answer them again.
|
||
|
||
Setting: `sesle_model` (default on).
|
||
|
||
Audit follow-up (11 Oct 2026, same version number; `GET .../status` now also returns `kod`, the first 8 hex digits
|
||
of `bridge.py`'s SHA-256, to tell builds apart):
|
||
|
||
- Numbers said as words ("opus beş beş", "üç nokta sekiz") and a trailing "ve devam et" no longer drop the command.
|
||
- Picking the same model again in the list does not clear a spoken route (Codex sends the same name), so the
|
||
bridge's answer says how to go back. A route that is in use no longer expires after 7 days.
|
||
- A command is turned into a "model was changed" note only if it took effect; a mid-turn note also reaches a
|
||
subscription model when hook messages follow it.
|
||
- Combos: a Claude member selects the stricter token estimate; one unknown member no longer switches the overflow
|
||
guard off for a known small member; a combo dropped by the peer after the request was sent is not resent.
|
||
- An old `9r/…` name without a route gets a local 503 instead of going to chatgpt.com; upstream error bodies
|
||
reach Codex whole, with `Retry-After`.
|
||
- The last model list is also served when chatgpt.com cannot be reached at all, and is cached when the answer
|
||
was gzip-compressed. The old `display_map` setting is no longer read (subscription models keep their names).
|
||
- Log lines carry the date; the access-token tail is no longer logged.
|
||
|
||
### Work handoff instruction (prepared, off by default)
|
||
|
||
`is_hatti.py`: when `aiturk_hat` is enabled and has a `komut`, the bridge prepends a developer message ("small work
|
||
yourself; larger work goes to AITURK via this command") to requests whose `originator` header is `Codex Desktop`.
|
||
The Codex CLI (`codex-tui`, `codex_exec`) never receives it and nothing is written to any file it reads. Each new
|
||
`originator` value is logged once (`istemci goruldu`).
|
||
|
||
## Version 7 (IDE beta.26): external models behave like subscription models
|
||
|
||
Measured on 10 Oct 2026 with the unmodified Codex binary (`codex exec`, same conversation, model switched per turn).
|
||
|
||
| Area | Before | Now |
|
||
| --- | --- | --- |
|
||
| Context window | every added model declared 272 000 | `pencere.py`: NanoGPT's own list for `nano/*`, a local table for `cc`/`cx`/`ag`, a combo declares its smallest member, unknown = 128 000, nothing above 272 000 (the summarizer's window). Codex then compacts by itself when switching to a smaller model. |
|
||
| Overflow | the target answered with an empty stream; Codex retried five times | `pencere.sigdir` trims the middle of oversized old parts in the outgoing copy; if it still does not fit Codex gets `context_length_exceeded`. A limit reported by the target is learned (`pencere-ogrenilen.json`). |
|
||
| Tools | clones inherited `code_mode_only` + Responses Lite; GLM and Qwen failed 0/3 on simple tasks (empty answers, reasoning leaked into the answer) | `dis_arac_modu: "direct"` (default): classic tools. GLM 3/3, Qwen 2/3; Claude, `cx` and Gemini unchanged (6/6). `"kalip"` restores the old behaviour. |
|
||
| Intermediate messages | external assistant messages had no `phase`; Codex treats those as final answers | `SseRewriter` stamps `commentary` / `final_answer`. Deltas are never delayed; only the item's `done` event waits for what follows. |
|
||
| Interrupted answer | text written before a stop or an app exit was lost | `yarim.py` keeps it (memory only) when the *client* closes the stream and adds it as a note to the next user message of the same conversation, once. |
|
||
| Images | removed from every old message for all external models | kept for vision-capable targets (user messages, newest first, 1 MB total). |
|
||
| Summary brake | the 4th summary in 10 minutes failed the turn | three summaries in a row with no normal request in between, or eight in 10 minutes. |
|
||
| Gateway errors | `event: error` (e.g. `upstream 402`) looked like a cut stream | permanent ones become `response.failed` with a readable message; short upstream bodies are logged. |
|
||
| Start-up | the model list was fetched before listening; a slow gateway made the IDE give up after 10 s | window data comes from the cache at start and is refreshed in the background; the IDE waits 45 s. |
|
||
|
||
Not changed: subscription models, their order and names, and everything on the voice path (`/realtime/*` passes through untouched).
|
||
|
||
Settings in `bridge.json` (all optional): `dis_arac_modu`, `tasma_bekcisi`, `yarim_cevap`, `ozet_kopru.dongu_siniri`, `ozet_kopru.dongu_art_arda`.
|
||
|
||
A lone `.` sent as the user's message (used to apply a model change during a voice session) is answered by the bridge itself with "<model> seçildi." (the target is not probed, so it does not say "ready") and never reaches a model (`nokta_karsila`, default on). Targets that reject tool calls are remembered (`aracsiz-modeller.json`) and marked "araçsız" in the picker.
|