reflex-router
reflex-router
0.9.0 • Public • Published 21 hours ago
- Readme
- Code Beta
- 0 Dependencies
- 0 Dependents
- 13 Versions
reflex-router
Model routers ask "which model?" reflex also asks what happened next — and writes it down so the routing can eventually be judged against something.
`reflex` runs the real Claude Code behind a loopback proxy and does three things with it:
1. Decide. At the start of each piece of work, a fast decision model (TypeSafe Jev, or Laya running on your own machine) judges how much reasoning the task demands, not how long the message is. 2. Route (opt-in). When it is safe, and does not throw away the conversation's prompt cache for nothing, send the work to a cheaper model. Otherwise leave it exactly as it was. 3. Observe. For every decision, record whether it looked wrong afterwards (the next prompt reads like a correction, a test failed after an edit, an edit was undone) and read the result back with `reflex report`.
What this alpha does not do, so you don't have to find out: reasoning effort (`REFLEX_EFFORT`, off by default) is new and its effect on quality is unmeasured; no threshold in it is calibrated, because calibrating one needs far more data than one person's log holds; and no cost saving has been measured — the dollar figures below are list-price estimates over recorded token counts, not bills. See What reflex found, Status and Contributing data.
How it works
you ──► claude ──► reflex (127.0.0.1) ─────────────────► api.anthropic.com
(unchanged) front door + worker your credentials,
│ forwarded untouched
│ start of work only:
│ redacted task text, capped
▼
TypeSafe Jev
(decision backend)`claude` is started with its base URL pointed at reflex. A small front door owns the loopback port for the whole session and hands each request to a supervised worker; if the worker fails, the door forwards the request straight to the API. Claude Code's own hooks post to the same loopback door for outcome capture.
Decide → route → observe:
Ground rules (enforced by tests): fail open, so any error, timeout or surprise ends in forwarding your request unchanged; byte-identical passthrough unless a rewrite was deliberately applied; your Anthropic credentials are never read, stored or logged; loopback only; your `~/.claude/settings.json` is never edited (reflex passes one temporary `--settings` file and deletes it on exit); zero runtime dependencies (TypeScript, Node 20+).
Quick start
npm install -g reflex-router
That puts `reflex` on your PATH (Node.js 20+; alpha, see Status). The same package is attached to each GitHub release as a tarball.
Or from a checkout, if you would rather read it first:
git clone https://github.com/ziyacivan/reflex-router && cd reflex-router npm ci && npm run build && npm link
Give it a Jev key, either in the environment or in a file that survives new shells:
export TYPESAFE_API_KEY=apikey_...
or
mkdir -p ~/.reflex && (umask 077; echo 'TYPESAFE_API_KEY=apikey_...' > ~/.reflex/env) reflex doctor # what it would do, and where each setting came fromOr use TypeLLM as the decision backend: `export REFLEX_BACKEND=typellm TYPELLM_API_KEY=tl-sk-...` (or put the key in `~/.reflex/env`). On 500 synthetic tasks its plans matched Jev's on 93% (measurement, details); it has not yet been measured on real sessions.
Or keep decisions on your machine with Laya: no key, reflex starts and stops a loopback `laya-serve` for each session, offline. Install once, then set the backend:
uv tool install "laya[serve]" export REFLEX_BACKEND=laya reflex doctor
Uncalibrated, Laya keeps Opus for everything; reflex ships a calibration head fitted to reproduce Jev's decisions, with a safety margin towards Opus. It is conservative and less accurate than Jev: on 190 real prompts it moved 24 turns off Opus (Jev moved about half of them), and 1.6% of its plans were cheaper than Jev's (measurement, details).
`~/.reflex/env` holds `KEY=value` lines for `REFLEX_*` settings and a backend key. It is merged under your environment (the environment wins), and a file that holds the key but is readable by group or others is refused: reflex warns, ignores the file, and runs plain `claude`. `reflex doctor` says why.
Start in shadow mode, and look before you route:
1. `reflex` (everything you type after it goes to `claude` untouched). The default mode is `shadow`: nothing about your session changes, and `~/.reflex/decisions.jsonl` fills up. 2. Work as usual for a few sessions, then `reflex report` (`--since 2h`, `--usd`). Read section 3, "shadow vs actual": what reflex would have routed where. 3. When you are happy with that, opt in: `REFLEX_MODE=route reflex`. To route only subagent work and leave the main chat alone, add `REFLEX_MAIN_CHAT=never`. The status line then shows the model reflex actually sent and the session's estimated cost, e.g. `Reflex: ⇣ Sonnet 5.5 (asked Opus 5.5) · Est. Cost: $1.80` (a list-price estimate over recorded token counts). It shows no saving: pricing the routed model's tokens at the requested model assumes both write the same amount, which they do not, so the saving is measured only where `REFLEX_AB` held turns back (`reflex report --usd`, section 8). When the main chat's prompt cache is known it adds how long it has left and what the next turn pays more once it lapses (`Cache: 42m left (lapse Est. +$0.61)`: the whole context written again instead of read, list prices; yellow in the last five minutes), and for three turns after the main chat's context shrank, by how much (`Context: compacted 183k→41k`). On a subscription it shows the account's quota as Anthropic reports it (`Quota: 5h 31%, 7d 48%`), and when the recent pace would reach 100% before the window resets, roughly when (`5h 62% (limit in ~40m)`). It also shows one line below for each running subagent, changed or not (Claude Code's own model display keeps showing the one it asked for); `REFLEX_STATUSLINE=0` turns it off, and a status line of your own is never replaced. 4. `REFLEX_MODE=off reflex` is literally plain `claude`, with no proxy at all.
Here is what the report looks like. This is real output (`reflex report` on the archived first dogfood session, sections 3 and 6), with its conditions in docs/observations.md: one shadow session, 12 decisions, nothing was routed, and "% tokens" is the token share of each cell's own request, not a saving.
3. Shadow vs actual
new turns that reached the backend (n=12); tokens = input + output + cache read + cache write of that turn's own request
requested would route to turns % turns % tokens actually sent there
opus haiku 5 41.7% 42.7% 0
opus sonnet 1 8.3% 7.7% 0
opus opus 6 50.0% 49.6% 6 routed records (rewritten and accepted; includes pinned continuations): 0
6. Latency
Jev decision latency (nearest-rank percentiles):
n p50 p95
all 12 823 ms 1,136 ms
connection not logged 12 823 ms 1,136 ms
The full report has sixteen sections (0–15): a workflow profile of where your tokens went; decisions by kind and tier; `mass` vs `argmax`; shadow vs actual; guard refusals; fallbacks and breaker; latency (Jev by new vs reused connection, the decision wait and the upstream's first byte); outcome rates for routed vs unchanged turns, with sample sizes and an explicit "insufficient data" line below 20 windows; cost at list prices; side-call usage on its own line; cache writes by move type; unclassified side-call fingerprints; a side-call routing estimate; escalations; effort (what `REFLEX_EFFORT` decided and applied, with outcomes by arm); and the quota exchange rate (tokens, and with `--usd` list-price dollars, per 1% of the 5-hour and weekly quota, by week and by model; the quota is account-wide, so usage outside reflex lowers these figures). `reflex report --json` prints the same sections as one JSON object, keyed by section number, for scripting against. Settings, route-mode details and safety nets: docs/reference.md.
How it compares
Capabilities only. This table has no speed or savings figures, ours or theirs, and it does not claim reflex is better on any number. It compares reflex-router with two existing Claude Code routers as we read them in docs/prior-art.md (their READMEs and code at the commits named there); both projects move, so check theirs: jev-router (`0d39e5b`) and jcm-router (`083df7f`).
| Capability | reflex-router (alpha) | jev-router | jcm-router | | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Outcome capture (correction, failing test after an edit, reverted edit, per decision) | Yes. Record-only by default; REFLEX\_ESCALATE can raise a later turn's tier, off unless you set it | Not in what we read; cost only (§5) | Not in what we read; cost only (§5) | | Quality-aware reporting | reflex report: outcome signals for routed vs unchanged turns with sample sizes, next to a stated cost estimate | Status line showing scores (display only, §3) | Dashboard of cost, cache health and Jev latency; offline eval of decisions on hand-labelled prompts (§2) | | Shadow mode | Default mode; report compares would-route with actual | No | dry\_run | | Pluggable / local decision backend | DecisionBackend interface: TypeSafe Jev, or Laya started by reflex on loopback | Jev only | Jev only | | Per-agent pins | Pin per conversation, and per subagent id; state is session-scoped | State per session + first message, reused through the tool loop; no explicit subagent signal | Reuses the most recent routed turn's decision; subagents detected from the system prompt; turn key is prompt-based (§2) | | Cost guard | Yes: refuses a main-chat switch whose lost prompt cache would cost more than a limit (list prices) | No cost model; a fixed context-size rule | Yes: cache-aware, with a maximum switch cost | | Privacy controls | Allow-listed state sent to the backend, character budgets and secret redaction before anything leaves the machine, log files 0600 in a 0700 directory and size-rotated, a redacted 300-character prompt preview (off by default, switchable) | README says only the prompt is sent; the code also sends model, context size and available models (§3) | Journal keeps a 300-character prompt preview by default (switchable) and is not rotated (§2) |
What they have that reflex does not: jcm-router has a live dashboard; jev-router has a custom model picker. reflex's report is plain text. Ideas we took from both (retry with the original request when a rewrite is rejected, byte-identical forwarding, pinning through a tool loop) are credited in THIRD\_PARTY.md; no code was copied.
What leaves your machine
Full detail: docs/privacy.md.
Delegation hint (opt-in experiment)
Per-turn routing can only reach work that starts a turn or a subagent. In long single-turn sessions most tokens sit in the main chat's tool loop, which stays on the model the turn started on, so `reflex report` starts with a workflow profile: where your tokens went (new turns, tool-loop continuations, subagents, side calls) and at most how much of them routing could have touched.
`REFLEX_DELEGATE=1` (off by default; `shadow` or `route` mode) is an experiment in moving work into subagents, where routing applies. On every prompt you type (not on slash commands, Claude Code's own injected messages or subagent hand-backs), reflex's `UserPromptSubmit` hook answers with a fixed three-line hint, as hook context, asking Claude to hand exploration, multi-file reading, searches and test runs to subagents and keep the main conversation for synthesis and edits. The text is in src/delegate/hint.ts; its version id is logged on every decision record of the session. If the worker is down or anything fails, the hook gets no answer and the prompt goes on without the hint.
Risk: delegation can raise your total spend. A subagent starts with its own context and re-reads files and state the main conversation may already hold, and Claude Code's side calls continue as before. More delegation is only a win if the cheaper tiers it enables outweigh that. The workflow profile and the cost section show total tokens and dollars per user turn with and without the hint, with the number of sessions on each side, so you can see whether it paid on your work; we have no measurement of that yet.
Overrides, ceilings and known limits
What we have measured
Each line is in docs/observations.md with its conditions. These are single sessions on one machine, not benchmarks.
What reflex found
Findings from the first week's dogfooding, each with the session(s) it came from. These are the same docs/observations.md entries in prose form — read there for the full numbers and caveats.
These are single-session or single-day figures, same as the rest of this section: not benchmarks. Every number here is a list-price estimate over recorded token counts unless it says otherwise, and none of them is a bill.
Benchmarks
There are no benchmark or measured savings figures, on purpose. The closest thing is the maintainer's own route-mode log, read with the same command you can run:
reflex report --usd
Over three days of it (2026-09-22 to 09-24, 27 sessions, 2,209 requests, \~384M tokens), routing moved 96 requests down from Opus 5.5 to Sonnet 5 or Haiku 4.5, an estimated $3.76 less, and 38 requests up to Opus, an estimated $3.73 more: net about $0.03. The best single session was an estimated $2.07 less, all of it in subagents. 73% of the tokens were main-chat tool-loop continuations, so routing could touch at most 26% of them, and the cost guard refused 18 main-chat switches whose lost cache would not have paid back (median penalty $1.09). Condition: one machine, one person; some upward moves came from sessions that requested Sonnet or Haiku with `REFLEX_UPGRADES` on; all dollar figures are list-price estimates over recorded token counts, not bills (entry).
The command prices the same measured token counts at the model sent and at the model requested, at list prices, and says what it does not model (tokenizer differences, what the requested model's cache would have held, cache TTL, discounts, subscription limits). Treat any figure about savings, ours or anyone's, as an estimate until it is measured against a bill.
Contributing data
reflex has no telemetry. It makes exactly two kinds of network connection: to Anthropic, because that is your Claude Code session, and to the one decision backend you chose, to ask one question per start of work: TypeSafe Jev, TypeLLM with `REFLEX_BACKEND=typellm`, or with `REFLEX_BACKEND=laya` a `laya-serve` on `127.0.0.1` that reflex starts offline. There is no third. Nothing about your usage is sent anywhere, ever, and there is no setting that turns such a thing on.
Which is also the problem. Every threshold in reflex is tuned on one person's log, and the section above says what that is worth: one organic correction in 44 scored windows, and telling the two decision rules apart would need roughly 430 disagreement windows per arm — on the order of 2,000 decided main turns, hundreds of sessions. That is not reachable from one machine. It is very reachable from thirty.
So if you want to help, you send the data yourself:
reflex share # writes ~/.reflex/reflex-share-<date>.jsonl and prints exactly what is in it head -3 ~/.reflex/reflex-share-*.jsonl
`reflex share` is an allow-list, not a redactor: a field reaches the file only because it is named in src/report/share.ts, so a field added to the log in future is absent from a shared file until someone adds it deliberately. It writes a file and makes no network connection of any kind.
| In the file | Not in the file | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | Hashed session and conversation ids; reflex's own record ids (random UUIDs) | Any prompt, reply, code, command or file path — hashed or not, those fields are simply not copied | | Timestamps, tiers, model names, which tier was picked and sent, and the reason codes | Your Anthropic credentials or TypeSafe key (reflex never records them anywhere) | | The backend's answer: probabilities, confidence, rule, latency, version | Your machine name, user name, working directory or any environment variable | | Token counts, HTTP status codes, timings | Prompt previews, error text, side-call fingerprints | | Outcome counts (edits, bash runs, test runs), correction rule ids and scores, test-runner kinds | |
Read the file before you send it. If anything in it looks like something you would not want public, do not send it — open an issue saying what you saw instead. That is a bug in `reflex share`, and it is worth more than the data.
Attach it to a calibration data issue, with your Claude Code version, OS, the model you request and roughly what the work was.
The most useful thing you can do is run with `REFLEX_AB=0.2` for a while. It holds a random 20% of routable turns on the model you asked for, as a control arm, and tags both arms. It is the only setting that produces data supporting a causal read: every other comparison in the report is between turns the backend judged easy and turns it did not, which differ in difficulty before any outcome is measured. It costs you the cheaper tier on one turn in five.
Status
Alpha. Published on npm as `reflex-router` from 0.5.3; each version is also a tarball on GitHub releases (see Quick start). It works and it is covered by tests, but almost nothing in it is calibrated, and the honest summary is below rather than in a footnote.
npm ci npm test # typecheck + lint + offline tests (a guard fails any non-loopback connection) npm run test:live # needs a real TYPESAFE_API_KEY / TYPELLM_API_KEY; each test file skips itself without its key npm run build
Notes on what Claude Code sends, with redacted captures: docs/wire-format.md. Prior art: docs/prior-art.md. Changes: CHANGELOG.md.
MIT. See LICENSE and THIRD\_PARTY.md.
