Contributing? See CONTRIBUTING.md, SECURITY.md, and the release checklist.
Per-call model routing for Codex, driven by Jev (TypeSafe System One).
Jev chooses a model and thinking effort together for each model call, including continuations after tools. Every route uses standard speed. The objective is sufficient capability for the next decision with no unnecessary quota consumption.
Historical simulation: ≈ −60 % vs full Astra on 237 turns under the old policy. This is not measured Codex quota saved, nor evidence for the current policy — protocol and limitations in BACKTEST.md. Installing with an AI agent? Hand it AGENTS.md.
This repository is a self-contained monorepo. It embeds the maintained
Codex Router fork under router/ and connects Jev through its generic-provider
and curated-model extension points. No second Git checkout, submodule, or hidden
source clone is required. The imported fork and its provenance are documented in
ROUTER_FORK.md.
Codex ──▶ Codex Router (:4202)
├─ native models ──────────────▶ ChatGPT backend (your plan)
└─ "jev/auto" ─▶ LiteLLM ─▶ API forwarder
│
▼
jev_server.py (127.0.0.1:4319)
├─ compact decision state ─▶ Jev
│ └─ model + effort
│
└─ canonical Codex replay + decision
└─▶ local caller edge (shared native session)
└─▶ luna / terra / sol / astra
- Responses in, Responses out — no format conversion; the SSE stream is relayed verbatim, so tool calls, reasoning and compaction behave natively.
- Two independent projections — Jev sees only the bounded decision state.
The executing model receives the complete canonical replay held by Codex:
instructions, history or compaction handoff, tool calls and tool results.
The embedded router exempts the exact
jev/autoroute from conversation windowing and tool-result aging; those optimizations would otherwise destroy context before this server could relay it. - Fail-open — any Jev error keeps the turn alive (safe fallback route).
- Kill switch — a sentinel file routes without Jev, instantly.
- Quota fallback — only observed native quota exhaustion activates a locally discovered, compatible configured route. A second attempt is made only when a distinct candidate exists, and before emitting any retryable error to the client.
- Decision log — every routed turn is logged locally for calibration
(
~/.codex/codex-router/jev-router-live.jsonl), never published.
The shared contract in server/routing_policy.py gives Jev four independent
Choice questions in one request: whether the next call falls under the mandatory
Astra policy, the least expensive sufficient capability tier (Luna, Terra, Sol or
Astra), the minimum sufficient thinking depth (low through max), and a bounded
route lease (one_call, tool_chain or user_turn). The first
choice covers project architecture, independent final code review and risk-focused
review (security, auth/permissions, concurrency, migrations, public API compatibility
or material performance risks). Routine in-progress quality checkpoints, comparing
scores and fixing established findings use ordinary capability routing; the word
"review" alone does not force Astra. Code forces Astra when that policy choice is positive,
regardless of the ordinary tier choice; the independently selected effort is
preserved. Every pair uses standard speed, overriding an incoming Fast setting,
including retries and bypass modes.
There is no target distribution, keyword-to-model rule, low-confidence fallback to Sol, mechanical-step exception, or compaction pin. Outside the explicit mandatory-Astra policy, a valid pair of tier and effort decisions is applied unchanged even when options are close. Jev's conservative combined confidence and all four choice distributions are logged separately; neither is a measured probability that the selected model will successfully finish the task.
The native ladder is Luna → Terra → Sol → Astra. Terra covers routine bounded implementation with clear requirements, small local features, known-cause fixes and straightforward tests; Sol covers complex implementation and cross-file reasoning. These profiles are routing priors, not measured capability guarantees. Terra attempts are counted as native in reports; its ChatGPT credit estimate remains unknown until a verified credit rate is configured.
Policy split-v11-native-first-fallback judges remaining work rather than inheriting a
completed review's category. Explicit mechanical follow-through can use Luna;
implied intent, underspecified goals and autonomous investigation favor Sol.
The objective includes correction and clarification costs. There is no
keyword-based override or automatic model floor on conversation openings.
Short asks receive a bounded preceding task and assistant proposal without a
language-specific intent regex. Separate instruction/environment messages are
skipped. A tool-result batch carries total/error counts and at most three short
excerpts, prioritizing errors. The canonical executor replay is unchanged.
Continuous quality signals and independent reviews are complementary. A favorable
Jev quality score never cancels a required final/risk review; routing confidence
is not a code-quality score. This router classifies the next model call: it does
not install or invoke jev-review, run a scoring loop, or create a separate
reviewer. For a workflow using that tool, bound score-driven corrections to one
justified iteration (a second only with new evidence), keep tests authoritative,
and give the independent Astra reviewer the task and code before comparing its
findings with Jev's scores. Review isolation belongs to that workflow; the router
still forwards the full canonical conversation and does not strip scores from it.
The model descriptions are capability priors, not calibrated success rates. The policy must be evaluated on completed tasks, corrections, tokens and quota, not on a desired share of Luna calls or artificially high confidence. Schema checks and synthetic routing samples establish wiring, not equal-quality savings. A missing/invalid Jev response or a provider error still uses the separately logged technical fail-open route (Astra at medium); the manual kill switch and native-quota exhaustion are operational bypasses, not Jev decisions.
This shape follows the useful parts of the surrounding router ecosystem: TypeSafe recommends small named state fields containing only relevant evidence and warns that irrelevant state reduces accuracy (State, Jev 1.13). ReflexRoute similarly supplies explicit candidate priors plus a few retrieved task examples, while LiteLLM's Jev classifier uses explicit tier criteria and treats short replies using their conversation context. By contrast, RouteLLM is a useful benchmarking reference but its published router path is primarily trained and calibrated around the current user prompt. The local policy therefore keeps a small adaptive task summary instead of either the whole thread or a blind last-message-only view.
The native model ladder is the policy unless the ChatGPT usage window is exhausted (manual sentinel file, or an automatic flip on a 429 / usage-limit response, which also retries the failed call). OpenAI remains strictly first while the native allowance answers: provider discovery is not even run on healthy turns.
While dry, the embedded router derives at most two candidates from the models
that this machine has configured, enabled and exposed. jev/auto is always
excluded to prevent recursion. Hidden, cooled-down, context-too-small and
capability-incompatible routes are removed. A local Ollama model is eligible
only when its runtime currently answers and its persisted real-Codex check says
agentCapable=true; LM Studio additionally has to report that exact model from
its live /models endpoint. Qualify any published local route explicitly with
router/bin/control failover qualify <local-model-slug>. The expensive real
Codex check is never launched inside a user turn.
The result is cached locally for 30 seconds. None of this inventory is sent to
Jev or added to its paid input.
JEV_FALLBACK_STANDARD and JEV_FALLBACK_FRONTIER remain explicit operator
overrides. If either is set at service startup, the static standard/frontier
order replaces discovery; a duplicate target is never retried as its own
sibling.
An automatic flip lasts until the instant the edge announced for the window reset, so the first call after the quota returns is served by the native model ladder again; when a refusal announces no instant it falls back to a 30-minute re-probe, and a week is the ceiling on anything a refusal claims. It is cleared by the first successful native call, and the manual sentinel file is never auto-cleared.
Two details keep the substitute transparent. The decided depth travels with the
call, mapped onto the fallback ladder — low stays low, medium and high become
high, xhigh or above become max — because those models declare three rungs
where the native model ladder exposes five, and the API forwarder clamps the value once more
onto the route's own ladder. Each fallback attempt is an exact route, preventing
the parent failover from looping through Jev. When two distinct targets exist,
a retryable failure is retained until one sibling attempt completes. Otherwise
the original refusal is returned once.
A third detail keeps the relay legal for the Responses consumer in front of it.
A dry turn crosses the local edge, which encodes response ids, so the terminal
event of the stream the relay receives repeats the id under a fresh encoding.
Read as-is, that is a completion that renamed its own response, and the consumer
replaces the finished turn with an invalid_responses_stream error; the relay
therefore rewrites the terminal id onto the one response.created announced.
Native turns are untouched — their ids already match.
The router logs one JSON line per decision (~/.codex/codex-router/jev-router-live.jsonl).
server/report_routing.py turns that log into the routing/savings report — the
table a third party can reproduce on their own machine:
python3 server/report_routing.py --days 7 --policy current # current policy only
python3 server/report_routing.py --days 30 --json # all versions, JSONIt prints the served model distribution (luna/terra/sol/astra, plus the Codex-dry
external fallback when it took over: turns + %), the share of turns served by the cheapest
tier, the share of turns held below the confidence gate, the gates encountered,
median latency (end-to-end and Jev's own decision time), observed prompt-cache
reads by model and hashed session, and an estimate of the real cost against two
counterfactuals — every turn on gpt-6-astra, and every turn on gpt-5.6-sol.
New log entries record a versioned decision and each upstream attempt's model, effort, standard speed, terminal event and token usage when the provider reports it. Only numeric usage counters are retained. Unknown usage is not counted as zero, retries are retained, and reasoning tokens are already included in output. The report estimates standard ChatGPT credits from these observed tokens against all-Sol and all-Astra counterfactuals. External fallback calls are excluded from that comparison. These are published-rate estimates, not observed account debits; counterfactual token volumes and task quality have not been experimentally measured.
Historical entries without usage keep a separate fixed-volume API-rate proxy. Their logged Fast speed retains its surcharge instead of being repriced by the new policy. The old backtest is clearly labelled as a simulation. Current replay scripts share the live decision contract and reject a cache from another policy.
Routing is phase-scoped. Jev can keep the exact route for one call, clean
continuations of the same tool, or clean tool continuations within the current
user turn. A new user turn, error, compaction, changed tool chain or expired cache
TTL ends the lease and forces a fresh decision. Provider retries inside one call
retain that call's decision. Jev sees measured per-model cache evidence:
hot only after a real cache read, warming after a successful zero-read call,
and unknown when usage is absent, plus read percentage, age and measured or
estimated context size. This is a cost tie-breaker, not a capability ceiling.
Jev receives only a bounded decision dossier:
active task, step type, and—when
relevant—a short assistant-intent tail, tool name, tool-output tail or image
flag. Short context-dependent asks such as continue also receive one bounded
active-task summary from Codex's goal envelope or the preceding meaningful user
ask. The executing model receives the caller's canonical request in full, with
only the selected model, reasoning effort, standard service tier and required
streaming flag changed. For eligible Astra Responses requests, the selected
effort is inserted as a configuration_update immediately before the latest
user message; this preserves the stable request-level prefix. Requests using
automatic truncation/context management, compaction items, or an incompatible
shape fall back to the request-level effort.
Context continuity is unconditional: every selected model receives the complete
canonical request, so a cache miss can increase processed input but can never
remove conversation facts. Cache controls (prompt_cache_key and
prompt_cache_options) are forwarded unchanged. GPT-5.6+ automatically caches
the stable rendered prefix separately for each model: the first arrival on a
model may be cold, while a later return can reuse that model's earlier prefix.
There is no cross-model KV-cache handoff, because those tensors belong to the
weights of the model that produced them
(OpenAI prompt caching).
The report hashes session ids before logging them and shows actual
cache reads and writes. Use
python3 server/report_routing.py --days 7 --policy current to inspect Jev
input, leases, swaps, cache reuse and rate-card credit estimates.
Each model still owns an independent cache. Returning to a previously used model can reuse its prefix, but observed switches on 21 September 2026 reused only about one fifth of their input as cached tokens. The affinity signal makes that measured reprocessing cost part of Jev's next typed choice while preserving the complete canonical replay.
The server also answers typed questions directly, for local callers that bring
their own question set. The jev-browser-choice skill is the first one: it
turns an in-app-browser accessibility dump into one Jev choice question and
acts only on the validated element index, so the page never enters the model's
context.
All POST endpoints now require Authorization: Bearer <local Jev credential>.
Read that credential in memory from
~/.codex/codex-router/generic-provider-credentials/jev.key; never paste it into
shell arguments, logs or a URL. Existing /ask callers must add this header.
Browser-origin requests are rejected. Direct TypeSafe clients are unaffected.
Provision the local credential using the embedded router's protected credential transaction, without entering or displaying it:
node server/configure-auth.mjsDo this after registering the jev provider and before restarting Jev. Missing
credentials fail closed (503); invalid credentials return 401. The health and
model-list GET endpoints remain public on loopback.
Validation is the whole contract: a JSON-serialisable state under 120k chars,
at most 40 questions, each a noul, choice or score with its instructions
and criteria. The caller's state is never logged. 502 surfaces an upstream Jev
failure — 402 means the TypeSafe account is out of credits — and 503 means
no key is configured.
install.sh One-command installer for the complete stack
bin/ Unified Jev/router CLI
router/ Embedded Codex Router fork and its own tests
server/ Jev policy, relay, service, reports and tests
poc/ Tiering POC, shadow replay and backtest tooling
hook/ Explored callback alternative, kept for reference
ROUTER_FORK.md Fork provenance and ownership boundary
AGENTS.md Autonomous install and operations playbook
Prerequisites: macOS with Codex desktop, Node.js 22.19+, Python 3.11+, and a TypeSafe API key for Jev. No separate Codex Router checkout is needed. See the support and required CI matrix for the pinned Codex contract, cross-platform fork coverage, and optional browser setup.
1. Give the server your TypeSafe key — either
export TYPESAFE_API_KEY=... in the service environment, or:
echo 'TYPESAFE_API_KEY=your-key' >> ~/.hermes/.env # default env fileSet JEV_ENV_FILE=/absolute/path/to/custom.env in the service environment to
load a different env file first. If it is absent or contains no key, the server
falls back to ~/.hermes/.env, then ~/.jev.env, then TYPESAFE_API_KEY from
the process environment. A keyless startup emits a warning before routing
continues in fail-open mode.
2. Install the complete stack from this checkout in your Terminal:
./install.shThe installer uses router/ as the service source, preserves configured
providers, idempotently adds jev/auto, provisions the protected loopback
credential, enables native ChatGPT sharing, installs both launchd services, and
runs server/smoke.py. It never clones or updates another repository.
3. Quit and reopen Codex, then pick “Jev Codex Router” in the model picker.
Check the transport as well as the picker: jev/auto must reach the local
router, not OpenAI's native endpoint. A catalog entry or a
[model_providers.jev] declaration alone does not select that transport.
See transport troubleshooting
if Codex reports that jev/auto is unsupported with a ChatGPT account.
For dependency preparation without touching services or local state:
./install.sh --prepare-onlyThe unified CLI exposes the embedded runtime without changing directory:
bin/jev-codex-router router status
bin/jev-codex-router update
bin/jev-codex-router smoke
bin/jev-codex-router report --days 7Requests are capped at 64 MiB for Responses and 256 KiB for /ask, with a
15-second body-read deadline and at most 32 simultaneous connections. Large
canonical requests are rejected explicitly rather than silently truncated.
Logs are created as 0600, rotate at 8 MiB, and keep one backup per file.
New debug captures contain only shapes/counters, not prompts or raw output.
Existing historical captures are protected but not erased automatically.
Display signatures are disabled for JSON-constrained responses.
Validation from the Jev checkout:
python3 -m unittest discover -s server -p 'test_*.py'
(cd router && npm run check && npm test)
python3 poc/eval_routing.py # offline fixture/dossier validation
python3 poc/eval_routing.py --live # optional paid Jev-only calibration
python3 server/smoke.py # small end-to-end model call; checks running policyThe real Codex integration lane is fail-closed: it cannot pass by skipping a missing CLI binary. See docs/SUPPORT.md for required runtime versions and the full CI matrix.
Replay tools share the live dossier builder but remain user-turn simulations, not a reconstruction of every internal model call or a quality-equivalent savings benchmark. Short confirmations are retained; mixed-model turns without per-call attribution are excluded from the historical cost baseline.
| Action | Command |
|---|---|
| Watch decisions | tail -f ~/.codex/codex-router/jev-router-live.jsonl |
| See the picked model in the thread | every reasoning summary part carries the routed tag, separators on both sides: · 🧠sol:low · — one glyph per route: ⚡ luna (economical) · 🧠 sol (workhorse) · 🚀 astra (frontier) · 🌍 terra; external fallbacks show their own route |
| Show the model and thinking above every assistant message | touch ~/.codex/codex-router/jev-router.signature — a leading **🧠 sol · thinking: high** appears from the first text fragment, including commentary and unphased replies; remove the file to disable |
| Shadow mode (decide + log, serve astra) | touch ~/.codex/codex-router/jev-router.shadow |
| Debug counters (no raw content) | touch ~/.codex/codex-router/jev-router.debug |
| Kill switch (no Jev → frontier) | touch ~/.codex/codex-router/jev-router.off (delete the file to re-enable) |
| Force the Codex-dry fallback | touch ~/.codex/codex-router/jev-router.codex-dry (delete the file to return to luna/terra/sol/astra) |
| Inspect the dry auto state | cat ~/.codex/codex-router/jev-router.codex-dry.json (reason + expiry; auto-cleared by the next successful native call) |
| Update the complete monorepo | bin/jev-codex-router update |
| Hide the model | router/bin/control picker set jev/auto hide |
| Disable the provider | bin/jev-codex-router router providers generic disable jev |
| Revoke native sharing | bin/jev-codex-router router chatgpt-session disable |
| Service status | launchctl print gui/$(id -u)/com.thibaultsaintjean.jev-router |
bin/jev-codex-router update fetches this repository's origin/main, updates
the complete checkout, then runs the root installer. It never updates the
embedded fork from a separate upstream checkout.
After an update, verify nothing was lost:
bin/jev-codex-router router providers generic list # shows: SHOW jev
cat ~/.codex/codex-router/model-picker.json # jev/auto in "visible"
curl -s http://127.0.0.1:4319/health- The router's local edge requires
stream: true— the server always forces it. - The edge returns SSE with no Content-Type header; the server re-emits
text/event-streambecause the API forwarder picks its parser from it (otherwise it tries to JSON-parse the stream and fails withinvalid_responses_response). - The shared ChatGPT session authorization has a validity window; re-run
chatgpt-session enableif native routing stops after a while. - Code comments are in French for now (author's working language) — PRs welcome.
- No secrets in this repository. The server reads
TYPESAFE_API_KEYfromJEV_ENV_FILEwhen configured, the default env files, or the process environment; everything else stays on your machine. - The server binds
127.0.0.1only, talks to your local Codex Router only, and never logs prompt content beyond a short task excerpt used for calibration. - Local decision logs and replay data are git-ignored by default.
Early, but running in production on the author's setup. The split routing policy needs outcome calibration on real usage; the local decision and attempt logs provide observations, not quality labels.
MIT