pi-jev-model-router
Route pi prompts to task-appropriate model tiers with TypeSafe Jev typed judgments. Budget-aware, with automatic fallback.
Package details
Install pi-jev-model-router from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-jev-model-router- Package
pi-jev-model-router- Version
0.3.0- Published
- Sep 20, 2026
- Downloads
- 574/mo · 574/wk
- Author
- da-vinci-noob
- License
- MIT
- Types
- extension
- Size
- 130.7 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/da-vinci-noob/pi-jev-model-router/main/assets/decision-entry.png",
"extensions": [
"./extensions/pi-jev-model-router/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-jev-model-router
A pi extension that routes every prompt to a task-appropriate model using TypeSafe Jev (System One) typed judgments. You type normally; before the turn starts, Jev reads the request and answers four narrow questions, code composes those into a capability tier, applies your budget policy, and pi switches to the matching model.

- Task-aware — planning goes to reasoners, implementation to coding specialists, chat to cheap fast models.
- Budget-aware — daily/monthly caps downgrade tiers automatically instead of overspending.
- Resilient — each tier is a candidate chain; if a model is unavailable or unauthenticated, the next one is used.
- Visible — the transcript records the chosen model and the exact reason (kind, complexity, capability, reasoning, budget pressure).
- Fails open — a missing key, timeout, or unknown model just warns and runs your prompt on the current model.
How it works
you type a prompt
│
▼
Jev (one request, 4 parallel questions)
• task_kind choice: plan / implement / debug / refactor / review / research / explain / operate / write / chat
• complexity score: trivial → architectural
• capability_deserved score: minimal → maximum (price ignored)
• needs_deep_reasoning noul: yes/no probability
│
▼
code composes the decision
demand = 0.55·complexity + 0.45·capability (+ reasoning nudge)
demand = max(demand, kind floor) # planning/review never go cheap
confidence guard → budget guard → availability guard → cache guard
│
▼
pi.setModel(...) + pi.setThinkingLevel(...) → the turn runs on that model
Jev judges the task, code owns the budget. Changing your spend caps never invalidates the judgment, and the judgment stays a pure semantic read of the request.
Requirements
- pi (
@earendil-works/pi-coding-agent) - Node.js 20+
- A TypeSafe API key with access to
jev-latest— https://typesafe.ai
Install
1. Install pi
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
Verify with pi --version. See the
pi quickstart
for other installation methods and authentication options.
2. Install this package
From npm (recommended):
pi install npm:pi-jev-model-router
From a pinned git ref:
pi install git:github.com/da-vinci-noob/pi-jev-model-router@v0.3.0
From a local checkout:
pi install /absolute/path/to/pi-jev-model-router
Try it once without installing:
pi -e npm:pi-jev-model-router
Manage it like any other pi package:
pi list # show installed packages
pi update --extensions # update packages
pi remove npm:pi-jev-model-router
3. Set your TypeSafe API key
export TYPESAFE_API_KEY=...
Add it to your shell profile to persist it. You can also set apiKey directly in
the config file (see below).
4. Use it
Start pi and type a request. Before the turn runs, the router announces the decision:
jev-router → high openai/gpt-5.3-codex
implement · complexity 1.70/3 · capability 1.55/3 · reasoning 0.82 → high
The status bar shows jev-router:<tier> · $spend · %cap, or jev-router:off when
disabled. No configuration is required — sensible defaults are built in.
Commands
| Command | What it does |
|---|---|
/jev-router |
Status: mode, spend, tier chains, kind specialists, last decision |
/jev-router on / off |
Enable/disable routing |
/jev-router mode auto|confirm|notify |
auto switches silently; confirm asks each turn; notify only tells you |
/jev-router budget daily 10 |
Session-only daily cap (persist it in the config file) |
/jev-router budget monthly 150 |
Session-only monthly cap |
/jev-router why |
Re-run Jev on the last prompt and show the full judgment + decision trace |
/jev-router revert |
Switch back to the model that was active before the last auto-switch |
/jev-route <text> |
Classify arbitrary text and show the recommendation without switching |
The model can also call the jev_route tool to ask for a tier recommendation for
a subtask.
What you see
Every decision is a durable entry in the transcript, so the chosen model and its justification are always available:
jev-router → high openai/gpt-5.3-codex
implement · complexity 1.70/3 · capability 1.55/3 · reasoning 0.82 → high
· budget 12% of cap → one tier down
The glyph encodes the action: → switched, = already active (stickiness),
• notify-only mode, × skipped. Expand the entry for the raw judgment: kind
and confidence, complexity, capability deserved, deep-reasoning probability,
composed demand, and budget pressure. Entries are stored in the session but never
sent to the LLM, so they cost no context.
Prompts that are deliberately not routed are shown too, so behaviour is never silently missing:
jev-router · not routed
acknowledgement — staying on the current model
using openrouter/~anthropic/claude-opus-latest
Which models it uses by default
The defaults target OpenRouter, because it exposes a large catalogue through a single provider id. Four capability tiers, each an ordered fallback chain:
| Tier | Order tried |
|---|---|
quick |
~google/gemini-flash-latest → ~openai/gpt-luna-latest → ~z-ai/glm-flash-latest → ~deepseek/deepseek-v4-flash-latest |
standard |
~deepseek/deepseek-pro-latest → openai/gpt-5.4-mini → ~z-ai/glm-latest |
high |
~anthropic/claude-sonnet-latest → ~openai/gpt-terra-latest → ~google/gemini-pro-latest → ~x-ai/grok-latest |
premium |
~anthropic/claude-opus-latest → openai/gpt-5.5 → ~openai/gpt-astra-latest |
Plus kind specialists, tried before the tier chain when the chosen tier is high
enough (minTier):
| Kind | Specialists |
|---|---|
plan |
~anthropic/claude-opus-latest (≥premium) → ~openai/gpt-astra-latest (≥premium) → ~openai/gpt-terra-latest (≥high) → ~google/gemini-pro-latest (≥standard) |
implement |
openai/gpt-5.3-codex (≥standard) → moonshotai/kimi-k2.7-code → ~anthropic/claude-sonnet-latest |
debug |
openai/gpt-5.3-codex (≥standard) → ~openai/gpt-terra-latest (≥high) → ~anthropic/claude-sonnet-latest |
refactor |
openai/gpt-5.3-codex (≥standard) → moonshotai/kimi-k2.7-code |
review |
~anthropic/claude-opus-latest (≥high) → openai/gpt-5.5 (≥high) → ~anthropic/claude-sonnet-latest |
research |
~google/gemini-pro-latest (≥standard) → moonshotai/kimi-k3 → ~openai/gpt-terra-latest |
explain |
~google/gemini-flash-latest (≥quick) → openai/gpt-5.4-mini → ~google/gemini-pro-latest (≥standard) |
operate |
openai/gpt-5.4-mini (≥standard) → ~deepseek/deepseek-pro-latest |
chat |
~google/gemini-flash-latest → ~openai/gpt-luna-latest → ~z-ai/glm-flash-latest |
write |
~google/gemini-flash-latest (≥quick) → openai/gpt-5.4-mini → ~anthropic/claude-sonnet-latest (≥standard) |
Provider-maintained ~...-latest aliases are used wherever they exist, so the
chains follow new model releases instead of going stale.
Run /jev-router to see this for your own setup, with a ✓/✗ per route
showing what is actually available and authenticated.
Extending to more models and providers
The router is provider-agnostic: it only references models that pi already knows about, so if pi can use a model, the router can route to it. You are never limited to OpenRouter — the defaults are just a convenient starting point.
0. Drop the built-in models entirely (optional)
By default your config is merged over the built-in chains, so a tier you don't mention keeps its defaults. If you'd rather start from nothing and use only your own models, set:
{
"useDefaultModels": false,
"routes": {
"quick": [{ "provider": "openrouter", "model": "~z-ai/glm-flash-latest" }],
"high": [{ "provider": "anthropic", "model": "claude-sonnet-4-5" }]
},
"kindModels": {
"implement": [{ "provider": "openrouter", "model": "moonshotai/kimi-k2.7-code", "minTier": "standard" }]
}
}
With useDefaultModels: false:
- the built-in
routesandkindModelsare gone — not available as a base or as fallback; - tiers or kinds you don't configure are empty, and the router simply skips them (it never invents a model);
- everything that isn't a model list still applies —
endpoint, timeouts,budget,cache, and thekindMinimumTierfloors.
/jev-router prints built-in models: off (config-only) and marks empty tiers
as (none configured). If a tier you need is empty, pi warns on session start.
1. Find the model ids pi knows
pi --list-models # all providers
pi --list-models | grep anthropic
pi --list-models | grep -E 'gpt-5|codex'
The first column is the provider id and the second is the model id. Those are exactly the two fields the config uses.
Providers not yet configured can be added with
pi /login, an API key environment variable, or a custom provider registered by another extension — including local servers such as Ollama or llama.cpp. See the providers docs.
2. Point the tiers at your models
Create ~/.pi/agent/pi-jev-model-router.json (or <project>/.pi/pi-jev-model-router.json).
Anything you set is merged over the defaults, per tier.
{
"routes": {
"quick": [
{ "provider": "openrouter", "model": "~google/gemini-flash-latest" }
],
"standard": [
{ "provider": "anthropic", "model": "claude-haiku-4-5" }
],
"high": [
{ "provider": "anthropic", "model": "claude-sonnet-4-5" },
{ "provider": "openai", "model": "gpt-5.4" }
],
"premium": [
{ "provider": "anthropic", "model": "claude-opus-4-5" },
{ "provider": "openai", "model": "gpt-5.5-pro" }
]
}
}
Each tier is a candidate chain, tried top to bottom. The first model that
exists in pi's catalogue and is authenticated wins; if none are usable the
router steps to the nearest tier instead of failing. Add "thinkingLevel" to any
entry to pin it ("off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
pi clamps it per model).
Mixing providers is fine — put an OpenRouter model and a direct-Anthropic model in the same chain.
3. Add or change task specialists
{
"kindModels": {
"implement": [
{ "provider": "openrouter", "model": "openai/gpt-5.3-codex", "minTier": "standard" },
{ "provider": "anthropic", "model": "claude-sonnet-4-5", "minTier": "standard" }
],
"plan": [
{ "provider": "openai", "model": "gpt-5.5", "minTier": "high" },
{ "provider": "anthropic", "model": "claude-opus-4-5", "minTier": "premium" }
]
}
}
minTier gates a model to a minimum capability tier, so a specialist is only
used when the judgment justifies it. Among eligible specialists, the one whose
minTier is closest to the chosen tier wins — a cheap specialist never wins a
premium-quality turn.
4. Set the floor per task kind
{
"kindMinimumTier": {
"plan": "high",
"review": "high",
"implement": "standard",
"debug": "standard",
"chat": "quick"
}
}
Known kinds: plan, implement, debug, refactor, review, research,
explain, operate, write, chat.
5. Change which task kinds exist
The kinds are defined in extensions/pi-jev-model-router/config.ts
(TASK_KINDS) and passed to Jev as the choice criteria. Edit the labels, add
domains of your own (for example data, infra, legal), then add matching
entries under kindModels and kindMinimumTier. Because the question is a Jev
choice, the option set is the taxonomy — no retraining, no prompt parsing.
Config resolution order
Later sources win:
- built-in defaults
~/.pi/agent/pi-jev-model-router.json<cwd>/.pi/pi-jev-model-router.json(trusted projects only)- env:
TYPESAFE_API_KEY,JEV_ROUTER_MODE(auto|confirm|notify),JEV_ROUTER_OFF=1
A full example lives at
extensions/pi-jev-model-router/pi-jev-model-router.example.json.
Run /reload after editing config.
Budget management
Cost is accumulated from each assistant message's computed cost into
~/.pi/agent/pi-jev-model-router-state.json, together with Jev request counts.
pressure = max(today ÷ dailyUsd, month ÷ monthlyUsd)
pressure ≥ softRatio(default0.7) → drop one tierpressure ≥ hardRatio(default0.9) → forcequick, unless the demand score is ≥ 2.5 (clearly architectural), which may stay atstandard
{
"budget": {
"dailyUsd": 5,
"monthlyUsd": 100,
"softRatio": 0.7,
"hardRatio": 0.9
}
}
Omit either cap to disable that dimension. Caps are policy, not a hard stop — they redirect routing, they do not block turns.
Prompt-cache awareness
Switching models discards the provider's prompt cache, and caches are per-model. The next request then re-reads the entire prefix — system prompt, tool schemas, and conversation — at the new model's full input rate. Cache reads are ~10% of input on the major providers, so a switch effectively costs the whole context once, and a switch back costs it again. On a 50k context that is roughly $0.10 on Sonnet; at 200k, roughly $0.45.
The router therefore gates switches instead of making them freely:
- Cache penalty cap — estimates the miss (
contextTokens × new model's input rate, minus the cached rate) and refuses the switch when it exceedsmaxPenaltyUsd. - Dead-band — demand has to clear the current tier's band (
tier ± 0.5) bydeadbandbefore a tier change is considered, so prompts hovering on a boundary stop flapping between two models. - Big-jump bypass — a tier jump of
bypassTierDeltaor more still switches, because that is a genuine capability change rather than a marginal one. - Same-tier swaps count too — a specialist swap such as Sonnet → Codex at the same tier is still a model change, and is priced the same way.
Held turns still record the decision, and say so:
jev-router = high openrouter/~anthropic/claude-sonnet-latest
explain · complexity 0.40/3 · capability 0.30/3 · reasoning 0.20 → standard, held on high to keep the cache
· cache penalty ~$0.186 on 120k tokens — keeping the warm cache
The estimate is a lower bound — real cacheable prefixes include the system prompt
and tool schemas, which contextTokens does not count — and it is skipped
entirely when a model's pricing is unknown, so it never blocks on guesses. Set
cache.aware: false to restore unconditional switching.
{
"cache": {
"aware": true,
"deadband": 0.25,
"maxPenaltyUsd": 0.05,
"bypassTierDelta": 2
}
}
Configuration reference
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
Master switch |
useDefaultModels |
true |
false drops the built-in model chains so only your config's models are used |
mode |
"auto" |
auto | confirm | notify |
apiKeyEnv / apiKey |
TYPESAFE_API_KEY |
TypeSafe credentials |
endpoint |
https://api.typesafe.ai/v1/systemone |
Evaluation endpoint |
jevModel |
"jev-latest" |
Jev model alias |
timeoutMs |
3500 |
Jev request timeout (retries 429/529) |
minPromptChars |
12 |
Below this, a prompt counts as a continuation (a short first message is still routed) |
historyTurns |
4 |
Conversation turns included as Jev state |
confidenceThreshold |
0.34 |
Below this, fall back to standard instead of spending premium |
stickiness |
true |
Keep the current model when it is already the chosen one |
routes |
see above | Capability tier candidate chains |
kindModels |
see above | Task-specialist chains with minTier |
kindMinimumTier |
see above | Per-kind floor tier |
budget |
no caps | Spend policy |
cache |
aware, cap $0.05, deadband 0.25 |
Prompt-cache-aware switching |
stateFile |
~/.pi/agent/pi-jev-model-router-state.json |
Spend ledger |
Failure behaviour
Routing never blocks your turn. A missing key, network error, timeout (default
3.5 s, retried on 429/529), or unknown model means: warn in the status line and
run the prompt on the current model unchanged. Prompts starting with /, pure
acknowledgements (yes, continue, …), and messages sent by other extensions
are never routed.
Publishing to pi.dev/packages
The pi package gallery is built from npm: it lists
packages that are tagged with the pi-package keyword. There is no separate
submission form — publishing to npm is the submission.
This repository is already prepared for it:
package.jsoncontains"keywords": ["pi-package", ...]package.jsoncontains apimanifest pointing at the extension entry pointpackage.jsoncontainspi.image, which the gallery uses as the preview card
To publish:
# 1. Log in to npm (once)
npm login
# 2. Sanity-check what will be shipped
npm pack --dry-run
# 3. Publish
npm publish --access public
Then:
- The gallery indexes it on its next crawl (usually minutes; allow a few hours).
- Check the listing at
https://pi.dev/packages/pi-jev-model-router. - Anyone can then install it with
pi install npm:pi-jev-model-router.
Releases: bump version in package.json, commit, tag, and re-run
npm publish. Keep the image URL pointed at a released tag or main so it
never 404s.
If the name is taken, publish under a scope (@yourname/pi-jev-model-router)
— the gallery indexes scoped packages too; install with
pi install npm:@yourname/pi-jev-model-router.
Git-only distribution also works (pi install git:github.com/user/repo@v1),
but only npm packages appear in the gallery.
You can also add a GIF or MP4 demo via pi.video (MP4 only, takes precedence
over image).
Development
git clone https://github.com/da-vinci-noob/pi-jev-model-router
cd pi-jev-model-router
# load the package into a throwaway pi run (ignores auto-discovered extensions)
pi -ne -e "$PWD" -p "Explain what an idempotency key does."
# or copy into the auto-discovered location for hot reload
cp -R extensions/pi-jev-model-router ~/.pi/agent/extensions/
Layout:
| File | Role |
|---|---|
extensions/pi-jev-model-router/index.ts |
pi wiring: events, commands, jev_route tool, model switching, transcript entries |
extensions/pi-jev-model-router/config.ts |
config types, defaults, layered loading, task taxonomy |
extensions/pi-jev-model-router/jev.ts |
TypeSafe HTTP client, question definitions, response parsing |
extensions/pi-jev-model-router/router.ts |
composition (decide), tier/kind chains, availability fallback |
extensions/pi-jev-model-router/budget.ts |
spend ledger, caps, pressure |
No runtime dependencies: the extension talks to TypeSafe with plain fetch. It
imports typebox (tool schema) and @earendil-works/pi-coding-agent (config
directory path), and loads @earendil-works/pi-tui lazily, only when the host
implements registerEntryRenderer. @earendil-works/pi-tui is declared as an
optional peer dependency, so hosts that don't ship it still install and run.
Compatibility with pi builds and forks
ExtensionAPI surfaces differ across pi versions and downstream forks (for
example omp). The extension probes the host at load time and degrades instead
of failing installation:
| Capability | If the host lacks it |
|---|---|
registerEntryRenderer or @earendil-works/pi-tui |
No transcript card; decisions still show in the status bar and notifications |
appendEntry |
Decisions are not persisted as session entries |
ctx.ui.notify / ctx.ui.setStatus |
Silently skipped |
ctx.ui.select |
confirm mode falls back to auto-switching |
ctx.modelRegistry.find / getAvailable |
Reports "model not available in this build" and leaves the current model in place |
registerCommand / registerTool |
Commands and the tool are not registered; event-driven routing still works |
Nothing in the extension throws during load if an optional API is missing, so
pi install, omp install, or any plugin validator will accept it.
License
MIT
