The same Jev transport and credential lookup also serve approval rules. Approval judgments are independent of Auto routing: they only evaluate pending approval requests and never choose or run a coding model.
Auto is the ⚡ toggle in the composer. When it is on, Modex asks a small, fast judge a few
typed questions about each request before the turn runs, then picks the backend, model,
reasoning effort, and fast mode for that turn — within limits you set — and shows a
one-line receipt in the transcript explaining why. The coding turn itself still runs only
through the claude and codex CLIs; nothing about Auto changes that.
The judge is Jev, TypeSafe's System One model. It does not generate text and never sees
your files; it answers typed questions about a compact digest of the request and returns
calibrated probabilities. Modex asks six questions in one request
(apps/desktop/src/main/engine/routing/judge.ts, question set v1):
| id | type | what it decides |
|---|---|---|
task |
choice | quick answer · small edit · bug fix · feature · refactor · investigation · review · ops · unclear |
complexity |
score 0–3 | one-step … cross-cutting, each level a concrete situation |
blast_radius |
score 0–2 | one file … could delete data / rewrite history / touch other systems |
wants_speed |
noul | the user signalled a quick turnaround matters more than thoroughness |
needs_deep_reasoning |
noul | doing it well needs multi-step reasoning about interactions |
depends_on_prior_turns |
noul | only when the thread has history: the request leans on it |
The state sent is the request (clipped to 4k chars), the last six transcript entries (user text clipped, a placeholder for each assistant reply, tool titles), and thread/project facts (backend, mode, plan, turn count, project name, branch). No file contents, diffs, or tool output are sent.
The key can live in any of these places; Modex checks them in this order and tells you which one won in Settings → Auto routing:
- Modex itself — paste it into Settings. It is encrypted with the OS keychain
(Electron
safeStorage: macOS Keychain, Windows DPAPI, or the Linux keyring) and written to~/.modex/app/secrets.jsonwith mode 0600. It is never instate.json, never in a build, and the renderer only ever sees the last four characters. TYPESAFE_API_KEYorJEV_API_KEYin Modex's environment.- The
jevCLI's config,~/.config/jev/config.json(JEV_CONFIGoverrides), written byjev config set apiKey …— one configuration for every tool on the machine. TYPESAFE_API_KEYfrom your login shell, because a Finder-launched app inherits no profile.
Any of those may hold a 1Password reference (op://Vault/Item/field) instead of a
literal key; Modex expands it in memory through op read at use time and reports a
locked vault as the problem rather than silently judging with the heuristic.
The judge itself is reached one of two ways (Settings → Judge transport):
jevCLI (default when installed): Modex runsjev run - --raw --compact --attempts 1with the payload on stdin and the resolved key in the child's environment only. That gives Modex the CLI's config, its error messages, andjev doctor, and keeps one code path for people, scripts, and agents. Install withnpm linkinTypeSafeAI/cli.- Built-in HTTPS (
POST https://api.typesafe.ai/v1/systemone): the same request from Modex's own process, used when the CLI is absent or the policy says so.
Transport selection is explicit: Auto prefers the CLI and may fall back to HTTPS; Always the jev CLI never uses HTTPS if its executable is unavailable; Always Modex's HTTPS call does not inspect the CLI. Saving changed judge transport, executable, or model settings invalidates resolved setup without clearing learned preferences or disrupting an already-running turn. General, posture-only, and no-op saves retain connection-test health.
"Test judge" sends one tiny question through whichever is active and shows the answer or
the API's own error sentence (a 402 with no credits, a 401 for a rejected key). A
rejection disables Jev for the session — no per-turn retry latency — until the key changes
or a test succeeds. With no key and no CLI, Auto still works on the built-in heuristic and
every receipt says so.
Pricing and limits, from the TypeSafe model page at the time of writing: jev-latest →
jev-1.13.0, charged per input token at $0.042 per million (output free), 32k tokens of
state per request. A routing call sends a few hundred tokens, so Auto costs well under a
hundredth of a cent per turn and typically answers in well under a second; a turn waits at
most 8 s for the judge before falling back to the heuristic.
Every question Modex puts to Jev goes through one module,
apps/desktop/src/main/engine/routing/decisions.ts: Auto routing, the follow-up suggestion and
approval rules, including Try it. It owns the key, the transport and the
session-wide switch-off. An auth or billing rejection seen by any of them turns Jev off for all of
them until the key or judge settings change or a test succeeds. Each caller still asks its own
questions and handles its own failure: routing falls back to the heuristic, follow-ups use the
heuristic pick, and approval rules ask a person.
Each kind of judgment has a versioned question set. The version goes up whenever its wording or choices change:
| Set | Version | Questions |
|---|---|---|
routing |
1 | the six above (routing/judge.ts) |
follow_up |
1 | follow_up (routing/followup.ts) |
approval |
1 | destructive plus one per language rule (approvals/gate.ts) |
Each request to Jev leaves a receipt in ~/.modex/app/decisions.jsonl, one JSON line per
judgment, written with mode 0600. A receipt records:
- the set and its version, the Jev model and transport, and when it ran and how long it took;
- the thread, or
previewfor Try it; - Jev's typed answers (choice and confidence, score and confidence, or noul), or the error.
It never holds the state sent to Jev: no request text, digest, command or path. Past 4 MB the file
starts over, and the previous file is kept as decisions.1.jsonl. The connection test in Settings
is not a judgment and leaves no receipt. A receipt that can't be written is logged once and never
changes a decision.
Jev's answers are raw judgments. The policy (routing/policy.ts) turns them into a route,
deterministically, and every step that changes the outcome adds a reason to the receipt:
-
Confidence gate. If Jev's confidence in the task kind is below
min_confidence(default 0.6), Auto keeps whatever the thread was already using and says so. A blank model means the CLI's advertised default. When no effort was requested, it uses that model's default effort or medium, matched to supported levels under the active ceiling. -
Target tier. Starts at the complexity score; +1 for deep reasoning; +1 for a wide or hard-to-reverse blast radius; a quick answer with low complexity pins tier 0; posture shifts one tier down (economy) or up (quality); the learned per-task offset is added; clamped to 0–3; capped at 2 once the daily premium budget is spent.
-
Backend. Stays put unless
allow_backend_switchis on and the thread has no existing CLI session. Even then it only moves to an allowed backend when the current CLI cannot reach the target tier or is unavailable. Mid-thread switches lose the CLI's context, so this is off by default; model switches have their own guard in step 7. -
Model. The highest rung at or below the target tier on that backend (
routing/catalog.ts). The ladder is rebuilt from each backend's model list: Modex's fixed Claude aliases (haiku 0 · sonnet 1 · opus 2 · fable 3) and Codex's live catalogue (gpt-5.5 0 · gpt-5.6-* 1 · gpt-6-luna/sol 2 · gpt-6-astra 3), with description hints for anything new and tier 2 for unknowns. Successful non-empty lists are cached for a minute. A failed or empty list retries on the next turn; discovery errors appear in receipts and any resulting stop message. -
Effort. Tier sets the base (low/medium/high/xhigh); deep reasoning bumps one; an explicit speed signal drops one;
max_effortcaps it; the budget caps it at high. The result snaps to an effort the model actually lists. Claude gets--effort, Codex gets the per-turneffortfield. Supported-effort matching and low-confidence routes also obey the ceiling. If an Auto turn cannot establish a supported effort within the limit, it stops before coding execution with an explanation instead of silently using an unknown provider default. Once the daily premium budget is spent, Auto also stops if the only usable model is top tier or a low-confidence judgment pins a top-tier model. Pinned premium Auto routes count toward the same daily limit. The offline mock backend has no reasoning-effort dimension. Blocked turns do not consume the routing budget or teach learned preferences. Blocked receipts read "Auto blocked". A manual model repair after a block does not teach a tier preference from an older successful route. -
Fast mode. Only when the user signalled speed, the task is not reasoning-heavy, the pick is tier ≤ 1, and the model offers it. Codex: the
fastservice tier for this turn only (serviceTierForTurn). Claude:--settings '{"fastMode":true}'for the session. Claude Code runs fast mode only on Opus 4.8 and newer Opus models, so onlyopusoffers it and this step never picks it on Claude. On a subscription, it also needs usage credits turned on, and it draws on them even while plan usage remains. Claude reports insystem/initandresultwhether fast mode is serving (fast_mode_state) and, if not, why (fast_mode_disabled_reason). When Auto asked for fast mode and Claude ran without it, the receipt drops "fast" and gives the reason, for example "Fast mode did not apply: fast mode requires usage credits (extra_usage_disabled)." -
Holding the model. On a thread with a CLI session, a switch re-reads the whole history without the prompt cache and drops the previous model's thinking. So Auto keeps the model while the cache is likely warm. It still chooses freely on the first turn, after the thread has been idle 60 minutes or more, when the last turn left under 20k tokens of context, and when you have picked a different model since the last turn. While the cache is warm it never moves down, and it moves up only to a model two or more tiers higher, or when Jev is at least 0.85 sure the request needs deep reasoning and the request does not lean on earlier turns. Effort still follows the request. A probe on 2026-10-12 (Claude Code 2.1.296, codex-cli 0.162.1) showed an effort change keeps the whole cache on both CLIs, while a model change re-read it from scratch. A kept model runs at the effort Auto chose for the model it would have picked. A model kept a tier below that pick runs at higher effort to make up for it, and the receipt says "Raised effort from medium to high to make up for staying below …". A model kept above the pick runs at the pick's lower effort. A held turn does not reuse the thread's last effort, so a raise does not carry into later held turns. The
max_effortceiling, the budget's high cap and the model's supported levels still apply. A raise to xhigh or max counts as a premium turn, as moving up to the bigger model would have. Fast mode is held, because neither CLI's fast mode could be measured. Limits still win: a model over the premium budget or no longer listed is not held. A held route reads "Auto kept", with the cache size and idle time in its reasons.
Changing the policy never requires re-asking Jev; the raw judgments are reusable.
Learning-file writes are best-effort after a safe decision: a write failure appears in the receipt without blocking the coding turn or changing its outcome. Premium counts and learning stay in memory for the session and are included in the next successful write. They cannot survive an app restart until persistence succeeds.
After a turn completes, the composer offers one next message. The candidates are fixed
strings in routing/followup.ts (verify, review, investigate, plan, explain, continue), so
nothing the judge says can become prompt text: it only picks a key, or none.
What leaves the process is followUpState: the request, the task kind from the heuristic,
plan and mode, and four booleans about the completed turn — whether tools edited files, ran
checks, whether any tool failed, and whether the answer mentions remaining work. Assistant
text, tool arguments and tool output stay local; the unit test asserts the request never
carries them. Jev is asked only on Auto threads, with the same confidence floor as routing;
a non-Auto thread, an offline judge, a low-confidence or unknown choice, and every error fall
back to fallbackFollowUp, which prefers a failed tool → investigate, plan mode → plan,
"next steps" → continue, edits → review (checks ran) or verify (they did not), quick answer
→ explain, else continue.
The runner caches one answer per transcript state, so the renderer can ask on every render without asking Jev twice for the same turn. A new send or Stop cancels and clears it, and a turn that ended in an error or a Stop offers nothing.
~/.modex/app/routing-fit.json (routing/fit.ts) keeps a per-task-kind tier offset:
- You pick a different model by hand on an Auto thread → strongest signal. Choosing a higher tier than Auto did nudges that task kind up by 0.34 tiers (three consistent overrides move it a full tier); a lower one nudges down. Modex confirms what it learned in a notice.
- An Auto turn fails → a small nudge up (0.1).
- Offsets are clamped to ±1.5 and rounded to the nearest half tier when applied, so one override never flips a decision on its own.
The file also counts premium turns per day (premium_turns_per_day bound) and keeps the
last 200 routing records with outcomes, which is the dataset a future calibrated fit model
(or a threshold sweep in .probes/) would train on. Settings → "Reset learning" clears it.
Both CLIs report token usage for every model call: Claude on each assistant message
(cache_read_input_tokens, cache_creation_input_tokens, input_tokens), Codex in
thread/tokenUsage/updated. Modex keeps two things from each turn: how the first call's input
split between cache reads, cache writes and uncached tokens, and the total input of the last
call, which is what the next turn re-reads. Sub-agent calls are not counted; they have their own
context.
The thread stores the latter as lastTurn in state.json, with the model, effort and fast mode
the turn ran on and when it ended. Its fast mode is what the CLI reported when it says (Claude
does), otherwise what Modex asked for. Auto's routing records in routing-fit.json add the idle time
and context size at decision time and the turn's cache figures afterwards, so the records show
what each switch actually cost.
Settings has separate General, Coding CLIs, Auto routing, Approval rules, and Advanced / demo
sections, with Save
and Cancel kept visible while the content scrolls. Auto routing exposes posture, effort
ceiling, minimum judge confidence, premium turns per day, fast-mode allowance, backend
switching, and whether new threads start on Auto, plus the judge model and allowed coding
backends. All of these are settings.routing in ~/.modex/app/state.json.
Ordinary settings are drafts until Save. Saving waits for persistence before publishing new in-memory settings; a failed save keeps both the active configuration and the draft available for retry. A view-refresh failure after persistence is reported as a refresh failure rather than claiming that the save failed. Cancel, Escape, and backdrop dismissal discard drafts and are blocked while an asynchronous settings action is pending. Draft edits and other actions are disabled until the current operation settles.
Credential Save/Clear and Reset learning are separate immediate actions: Cancel does not undo them. Learning reset requires confirmation. Keys remain encrypted outside state.json; the UI receives only masked key metadata.
The allowed coding-backend list governs switching destinations; it does not move an existing session or enable switching by itself. An empty list keeps the current backend. Legacy Mock entries remain available for offline routing until explicitly disabled in Advanced / demo; the menu identifies them separately from coding CLIs.
Test judge uses the saved judge configuration. If transport, executable, or judge model has an unsaved change, apply it before testing. Configured/untested, explicitly verified, and failed tests are distinct; results identify their configuration and are invalidated when that configuration changes. Opening Settings never performs a paid provider ping.
Modex drives coding agents only through their CLIs, so a routing target is any CLI that Modex has a backend for. Surveyed on 2026-09-25:
| CLI | headless protocol | approvals | model / effort control | verdict |
|---|---|---|---|---|
Codex (codex app-server) |
JSON-RPC over stdio, streaming items | server→client requests | model, effort, serviceTierForTurn per turn; live model/list with efforts and service tiers |
shipped; the richest surface |
Claude Code (claude -p) |
stream-json in/out | control_request on stdio |
--model, --effort low…max, fast mode via settings |
shipped |
| Gemini CLI | -p --output-format stream-json |
--approval-mode default/auto_edit/yolo/plan (no per-call prompt channel found) |
-m |
good next candidate once its permission prompts can be answered from a host |
| GitHub Copilot CLI | -p |
--allow-all-tools only |
--model (incl. auto) |
possible for chat/plan modes; no interactive approval path yet |
| opencode (installed build) | -p -f json |
none | none in headless | not a fit for agent mode |
| Local models | via Codex `--oss --local-provider lmstudio | ollama(LM Studio hasqwen/qwen3.5-9b` on this machine) |
Codex's | Codex's |
Recommendation: keep Claude and Codex as the routing targets now; add local models as a
Codex profile (codex --oss) rather than a new backend, so AGENTS.md's CLI-only rule
holds; evaluate Gemini CLI as the third backend when its headless approvals can be driven.
- Unit:
pnpm --filter @modex/desktop testcovers the heuristic judge, the policy table, the fit arithmetic, the router with a fake Jev transport and with Jev failing, and the runner applying a route before a turn. - End to end:
pnpm test:e2etoggles Auto on the mock backend and checks the receipt. - Live: export
TYPESAFE_API_KEY, start Modex, turn on Auto, send a request; the receipt reads "Jev 0.xx". Settings distinguishes configured transport from a successful explicit Test judge result; a routing receipt alone is not a claim that all provider capabilities or future availability have been verified.