@coresofthq/pi-litellm-autorouter
LiteLLM's Auto Router v2, running inside the pi coding agent
Package details
Install @coresofthq/pi-litellm-autorouter from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@coresofthq/pi-litellm-autorouter- Package
@coresofthq/pi-litellm-autorouter- Version
2.0.0- Published
- Oct 29, 2026
- Downloads
- 456/mo · 323/wk
- Author
- azzlack
- License
- MIT
- Types
- extension
- Size
- 292.5 KB
- Dependencies
- 0 dependencies · 0 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-litellm-autorouter
A pi extension that picks the cheapest model able to handle each prompt, automatically - in-process, no proxy required.
What it does
pi normally uses one model for a whole session, switched by hand with /model. That's wasteful: a trivial
question ("what does this env var do?") doesn't need the same model as a hard debugging session. This
extension adds a virtual model, autorouter/tiered. Select it
like any other model, and every prompt is classified before it runs and routed to the right tier - so cheap
prompts go to cheap models and hard prompts go to strong ones, automatically.
It's a TypeScript port of LiteLLM's Auto Router v2
(auto_router/complexity_router), adapted to run inside pi instead of behind a separate LiteLLM proxy
process. That means no extra network hop, and pi itself knows which model answered: the footer shows the
routed model next to the selection (tiered • medium → claude-sonnet-5-5 • high), each assistant message
records it, and /session lists the cost per physical model.
How it works
While autorouter/tiered is selected, pi asks the router for a physical model before every request. Each new
prompt goes through: extract → classify → override → select → route.
- Extract - strip
<system-reminder>blocks and pi's expanded/skill:blocks (harness plumbing, not something the user asked), pull out the current ask plus a few prior turns for context. - Classify - score the prompt into one of four tiers,
SIMPLE→MEDIUM→COMPLEX→REASONING. Three classifiers are supported:
heuristic(default) - a local, sub-millisecond weighted scorer (code presence, reasoning markers, technical terms, length, etc.), no API call.llm- a small model classifies the prompt against a rubric. Theagenticrubric preset is the default here, calibrated so routine engineering work (installs, multi-file edits, standard debugging) lands atMEDIUMinstead of being over-classified as top-tier, which is what a chat-tuned rubric does to agent traffic.decision- a decision model such as TypeSafe's Jev or Cloudflare's Clef answers one typedtierquestion with probabilities, instead of generating text.
- Override - a few signals outrank the classifier, in order: a one-shot
/autorouter next, an/autorouter pinor/autorouter off/--no-autorouterescape hatch, a question reply (a turn that only answers a question the assistant asked keeps the model it was asked with), a session-affinity pin (reuse the first turn's model for the whole session, if enabled),keywordTierRules(keyword → tier, literal or semantic), and a plan-mode floor (routes at least to a configured tier while a plan-mode extension or sentinel is active).escalationKeywordscan bump the result up exactly one tier — never down or to a caller-chosen model. - Select - a tier maps to one model or a pool of models (a uniformly random pick), or, with
adaptive: true, a Thompson-sampled pick across the pools weighted by learned quality, price, and distance from the classified tier. See Adaptive selection. - Route - resolve the chosen model against pi's own model registry and credentials (falling back down
the chain, then to
defaultModel, if a model has no credentials), hand it to pi with its thinking level, and record the decision (visible via/autorouter explainand the footer status). The selection staysautorouter/tiered; nothing callspi.setModel().
The model is chosen once per prompt and held for the whole agent run - it never switches mid-run. Only the first request of a prompt is classified; everything else stays on the model already serving the conversation, which keeps prompt caches and thinking signatures valid:
| Request | Routed to |
|---|---|
A prompt you typed (or sent over RPC, or with pi -p) |
Classified and routed |
| Tool-call continuations within the run | The model serving the run |
| Automatic retries, including after compaction for a context overflow | The model that failed |
| Steering and follow-up messages queued while the run is in flight | The model serving the run |
Prompts other extensions send with pi.sendUserMessage() |
The model serving the session |
| Compaction summaries and other requests outside the agent loop | The model serving the session, else defaultModel |
Routing rarely fails a prompt: a classifier error falls back per classifierFallback, an unusable model
falls down the chain, and when nothing in the chain is usable the request stays on the model already
serving the session. Only when there is no such model either does the request end with an error that
names the models it tried.
Thinking levels
autorouter/tiered offers every thinking level, from off to max. The level you select is the default for
tier entries that don't set their own: with autorouter/tiered • low, a plain "anthropic/claude-sonnet-5-5"
entry runs at low, while { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "high" } always runs at
high. pi clamps the result to what the routed model supports.
Installation
Requires pi 0.99 or later (virtual models).
pi install npm:@coresofthq/pi-litellm-autorouter
# then, inside pi: /autorouter init
/autorouter init reads the models pi can reach, ranks them by price, and writes a starter config to
~/.pi/agent/autorouter.json (or .pi/autorouter.json for a project-local override), then selects
autorouter/tiered for the session. Edit the tiers from there - price is only a proxy for capability, not the
same thing.
To route every session, pick autorouter/tiered in /model (or start pi with --model autorouter/tiered), or
make it the default in ~/.pi/agent/settings.json:
{ "defaultProvider": "autorouter", "defaultModel": "tiered" }
Selecting any other model takes the router out of the loop until you select autorouter/tiered again (or run
/autorouter on). With enabledModels / --models scoping, add autorouter/tiered to the list.
The minimum config
{
"defaultModel": "anthropic/claude-haiku-5-5",
"tiers": {
"SIMPLE": "anthropic/claude-haiku-5-5",
"MEDIUM": "anthropic/claude-sonnet-5-5",
"COMPLEX": "anthropic/claude-sonnet-5-5",
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "high" }
}
}
Model names are provider/model-id, exactly as pi's /model picker shows them. They must be physical models:
a virtual model, autorouter/tiered included, cannot be routed to and is skipped. defaultModel takes the same
shape as a tier entry, so it can carry a thinking level too:
{ "defaultModel": { "model": "anthropic/claude-haiku-5-5", "thinkingLevel": "medium" } }
Example: heuristic classifier (default)
Local, sub-millisecond, no API calls. Good starting point - this is what /autorouter init writes.
Full file: examples/autorouter.heuristic.json.
{
"defaultModel": { "model": "anthropic/claude-haiku-5-5", "thinkingLevel": "medium" },
"tiers": {
"SIMPLE": "anthropic/claude-haiku-5-5",
"MEDIUM": "anthropic/claude-sonnet-5-5",
"COMPLEX": "anthropic/claude-sonnet-5-5",
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "high" }
},
"keywordTierRules": [
{ "keywords": ["migration", "schema change", "security review"], "tier": "REASONING" },
{ "keywords": ["typo", "rename", "formatting"], "tier": "SIMPLE" }
],
"escalationKeywords": ["PI ESCALATE"],
"planMode": { "minTier": "COMPLEX" },
"sessionAffinity": { "enabled": false, "ttlSeconds": 3600 },
"questionReply": { "enabled": true }
}
Example: LLM classifier
Asks a small model to classify each prompt instead of scoring it locally - costs an extra round-trip per
turn, but reads intent rather than keywords. classificationRubric: "agentic" is the coding-agent-calibrated
rubric described above; classifierFallback: "heuristic" means a timeout degrades to local
scoring instead of stalling the turn. Generate this shape with the llm flag:
/autorouter init llm
Full file: examples/autorouter.llm.json.
{
"defaultModel": { "model": "anthropic/claude-haiku-5-5", "thinkingLevel": "medium" },
"classifierType": "llm",
"classifierLLMConfig": {
"model": "anthropic/claude-haiku-5-5",
"classificationRubric": "agentic",
"timeoutMs": 3000
},
"classifierFallback": "heuristic",
"classifierContextWindowSize": 3,
"classifierContextPerTurnChars": 200,
"classifierContextIncludeAssistantTurns": false,
"tiers": {
"SIMPLE": "anthropic/claude-haiku-5-5",
"MEDIUM": "anthropic/claude-sonnet-5-5",
"COMPLEX": "anthropic/claude-sonnet-5-5",
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "high" }
}
}
The classifier sees your last classifierContextWindowSize prompts as context, each cut to
classifierContextPerTurnChars, so a short follow-up like "yes, do it" is rated by the work it continues.
classifierContextIncludeAssistantTurns: true adds the assistant's latest text reply - the proposal a "yes"
answers - without taking one of those slots. It's off by default: answers to question tools are already held
on the model that asked, and your earlier prompts usually say what is being approved. Turn it on if the
assistant often proposes work in plain text. The same settings apply to the decision-model classifier.
Example: decision-model classifier (Jev, Clef)
A decision model doesn't chat: it answers typed questions about a request, with a probability for each
answer. With classifierType: "decision", the router asks one System One choice question named tier
(each tier's meaning as the criteria) and sends the same request payload the llm classifier gets
(current ask, prior turns, conversation depth) as the state. The call goes through one of pi's own
classifier models, straight to the service that hosts it, with the credentials pi already has.
model names the classifier model as provider/model-id, like every other model in this config. Hosted
Jev, with TYPESAFE_API_KEY set (full file: examples/autorouter.jev.json):
{
"classifierType": "decision",
"decisionClassifierConfig": { "model": "typesafe/jev-latest", "timeoutMs": 3000 },
"classifierFallback": "heuristic"
}
Cloudflare's Clef on Workers AI, with CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID set (full file:
examples/autorouter.clef.json):
{
"classifierType": "decision",
"decisionClassifierConfig": { "model": "cloudflare-workers-ai/@cf/cloudflare/clef", "timeoutMs": 15000 }
}
Any classifier model pi supports works, including ones a pi update or models.json adds later:
| Decision model | model |
|---|---|
| Jev, TypeSafe | typesafe/jev-latest |
| Jev, OpenRouter / Workers AI / Vercel AI Gateway / OpenCode | openrouter/typesafe/jev-1.14, cloudflare-workers-ai/typesafe/jev, vercel-ai-gateway/typesafe-ai/jev, opencode/jev-1.13 |
| Clef, Workers AI | cloudflare-workers-ai/@cf/cloudflare/clef, cloudflare-workers-ai/@cf/cloudflare/clef-flash, cloudflare-workers-ai/@cf/cloudflare/clef-omni |
| Clef, OpenRouter | openrouter/cloudflare/clef, openrouter/cloudflare/clef-flash, openrouter/cloudflare/clef-omni |
| OpenAI's Decisions API | openai/gpt-6-luna (needs OPENAI_API_KEY, not a ChatGPT login) |
pi lists more; see pi's classifier models. The table reflects pi 1.4's catalog - some entries (GPT-6 Luna, for one) need a newer pi than the 0.99 the router itself requires.
Key (decisionClassifierConfig) |
Default | Purpose |
|---|---|---|
model |
typesafe/jev-latest |
The classifier model, provider/model-id |
timeoutMs |
3000 |
A hard deadline: the call is abandoned and classifierFallback applies |
instructions |
built in | Replaces the question's instructions; the tier criteria stay |
circuitBreakerEnabled |
true |
One timeout skips the classifier, straight to classifierFallback, for the cooldown |
circuitBreakerCooldownSeconds |
30 |
After it, one prompt probes; success closes the circuit, failure restarts the cooldown |
There are no endpoint or key settings: pi owns the connection, so set keys with /login or the
provider's environment variable, and endpoints in models.json. Decisions are logged as
cause=decision_classifier, with the choice, its confidence and every tier's probability under signals;
/autorouter explain also names the model and the call's cost. A tier with no models counts as a
classifier failure, so classifierFallback decides instead. A decisionClassifierConfig left in place
under another classifierType is ignored with a warning.
Example: mixing providers
Tiers are independent, so each can name whichever provider does that job best - and the classifier can be a
third. Here Anthropic covers the cheapest and the hardest tiers, OpenAI the everyday ones, and a small Mistral
model classifies. Terra comes through a ChatGPT plan (openai-codex/, sign in with /login); with an OpenAI
API key instead, write openai/gpt-6.1-terra once pi's openai catalog lists it.
Full file: examples/autorouter.multillm.json.
{
"defaultModel": { "model": "openai-codex/gpt-6.1-terra", "thinkingLevel": "medium" },
"classifierType": "llm",
"classifierLLMConfig": {
"model": "mistral/mistral-small-2603",
"classificationRubric": "agentic",
"timeoutMs": 2000
},
"classifierFallback": "heuristic",
"classifierContextWindowSize": 3,
"classifierContextPerTurnChars": 200,
"classifierContextIncludeAssistantTurns": false,
"tiers": {
"SIMPLE": { "model": "anthropic/claude-haiku-5-5", "thinkingLevel": "medium" },
"MEDIUM": { "model": "openai-codex/gpt-6.1-terra", "thinkingLevel": "medium" },
"COMPLEX": { "model": "anthropic/claude-sonnet-5-5", "thinkingLevel": "high" },
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "xhigh" }
}
}
Every model named must be one pi can reach with its own credentials; a tier whose provider has no key falls
down the chain to the next usable candidate, and the decision log says so under fallback=.
Example: recommended
The same tiers as the mixed-provider example, with TypeSafe's Jev deciding the tier: one structured
decision-model call per prompt instead of a chat completion,
and a timeout or error still falls back to the local scorer. It also shows Jev the assistant's latest reply
(classifierContextIncludeAssistantTurns), so a "yes" to a proposal is rated by the proposed work.
Needs TYPESAFE_API_KEY, your Anthropic credentials and a ChatGPT sign-in for Terra. Full file:
examples/autorouter.recommended.json.
{
"defaultModel": { "model": "openai-codex/gpt-6.1-terra", "thinkingLevel": "medium" },
"classifierType": "decision",
"decisionClassifierConfig": { "model": "typesafe/jev-latest", "timeoutMs": 3000 },
"classifierFallback": "heuristic",
"classifierContextWindowSize": 3,
"classifierContextPerTurnChars": 200,
"classifierContextIncludeAssistantTurns": true,
"tiers": {
"SIMPLE": { "model": "anthropic/claude-haiku-5-5", "thinkingLevel": "medium" },
"MEDIUM": { "model": "openai-codex/gpt-6.1-terra", "thinkingLevel": "medium" },
"COMPLEX": { "model": "anthropic/claude-sonnet-5-5", "thinkingLevel": "high" },
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "xhigh" }
}
}
Use the heuristic classifier by default; switch to llm if the scorer keeps misjudging your traffic and you
can tolerate the extra latency. /autorouter init [provider] [project] [llm] accepts all three flags together
- e.g.
/autorouter init anthropic project llmscopes to one provider, writes to.pi/autorouter.json, and configures the LLM classifier in one go. There's no separate command to flip classifier mode afterwards; re-runinit, or editclassifierType(andclassifierLLMConfig) directly in the config file.
Options
All keys go in the same autorouter.json. Names are the camelCase spelling of LiteLLM's
complexity_router_config keys, so a config can move between the two systems.
Domain keywords for the heuristic scorer
The scorer's technicalTerms dimension counts hits against a built-in list of ~80 terms. That list is
calibrated against the dimension's thresholds and cannot be replaced (a technicalKeywords key is
rejected), but it can be extended:
{
"customTechnicalKeywords": ["kafka", "redis", "postgresql", "udp", "dns"]
}
Entries are appended in order and deduplicated case-insensitively against the built-in list, so listing
"TCP" when "tcp" is already built in changes nothing. Mirrors upstream's custom_technical_keywords.
Question replies
On by default. When the assistant asks something with a question tool (ask_user_question, ask_question,
ask_followup_question) and the next turn only answers it, the router holds the session's model instead of
classifying the answer.
Without this, a mid-task question is a downgrade trap. The prompt that started the work classified COMPLEX
and put the session on Sonnet; the assistant asks "Postgres or SQLite?"; the user types ok. Scored on its
own, ok is SIMPLE - so the session drops to Haiku, and Haiku is the model that then has to act on the
answer. The turn carries no information about the work, so the honest thing is not to score it at all:
autorouter: routing decision cause=heuristic_scorer, tier=COMPLEX, routed_model=anthropic/claude-sonnet-5-5
autorouter: routing decision cause=question_reply, tier=COMPLEX, signals=[question_reply (ask_user_question)], routed_model=(unchanged)
routed_model=(unchanged) is literal: on this path no candidate is resolved and the request goes to the
model that asked the question, so it is the one decision that cannot move the model even by falling back.
The footer reads autorouter: held (question reply). The tier it holds at is kept as router state on the
session branch, so it survives a resume, a fork, and compaction.
A turn has to pass both tests to be held:
- A question is open - the newest assistant message carries a question tool call. A tool that was answered inline and acted on is followed by further assistant messages, so it no longer qualifies; this can never fire on a terse prompt at the start of a session.
- The reply reads as an answer - it names one of the offered options (conclusive, at any length), or it
is short and does not open a unit of work.
ok,yes please,option 2,the first one,up to youanduse postgresare answers.now refactor the whole auth layer to use JWTis not: it leads with a task verb, so it is classified and routed like any other prompt even though a question was open.
Escalation still works from the held tier - PI ESCALATE in a reply raises one tier above the tier the
session was already on, not one tier above the reply's own trivial classification. A plan-mode floor entered
while the question was open also still applies, since a floor can only raise.
{
"questionReply": {
"enabled": true,
"toolNames": ["ask_user_question", "ask_question", "ask_followup_question"],
"maxReplyChars": 120
}
}
| Key | Default | Purpose |
|---|---|---|
enabled |
true |
"questionReply": false is shorthand for turning the whole thing off |
toolNames |
the three above | Replaces the list, so a custom question tool can be named on its own. Compared on letters and digits only, so askUserQuestion matches ask_user_question |
maxReplyChars |
120 |
Longest reply still treated as an answer. Deliberately tight: a new instruction is often shorter than it looks, and a generous cap would swallow exactly the turns that need their own decision. Naming an offered option bypasses it |
Semantic keyword matching
By default keywordTierRules match literally (word-bounded, case-insensitive). With
semanticKeywordMatching: true they match by embedding similarity instead, so a paraphrase with no keyword
in it ("help me roll out my k8s cluster") still hits a rule for "kubernetes deployment". Port of upstream's
semantic_keyword_matching / embedding_model / match_threshold.
{
"keywordTierRules": [
{ "keywords": ["kubernetes deployment", "container orchestration"], "tier": "REASONING" },
{ "keywords": ["hello", "thanks"], "tier": "SIMPLE" }
],
"semanticKeywordMatching": true,
"embeddingModel": "voyage/voyage-3-5",
"matchThreshold": 0.5,
"embeddingEndpoint": { "apiKeyEnv": "VOYAGE_API_KEY", "timeoutMs": 3000 }
}
- One route per tier, that tier's keywords as its utterances,
maxaggregation: a prompt matches a tier when it is close to any of the tier's keywords, and the closest tier wins if its similarity is at leastmatchThreshold. Keywords are embedded once per session; only the prompt is embedded per turn. - With semantic matching on, literal matching is not consulted (as upstream). An embedding failure -
timeout, bad key, endpoint down - yields no override and the prompt falls through to the classifier; the
decision records why under
signals. - pi has no embeddings API, so the call goes straight to an OpenAI-compatible
/embeddingsendpoint.embeddingModelisprovider/model-id; the base URL comes fromembeddingEndpoint.baseUrl, else the provider pi knows by that name, else a built-in table (voyage,openai,mistral,openrouter,together,fireworks,google). The key comes fromembeddingEndpoint.apiKeyEnv, else pi's key for the provider, else<PROVIDER>_API_KEY. - Requires
embeddingModeland at least one rule;matchThresholdis in[0, 1].
Adaptive selection
Off by default. With adaptive: true, a tier's pool is no longer sampled uniformly: every model gets a
Beta posterior per request type (code_generation, code_understanding, technical_design,
analytical_reasoning, writing, factual_lookup, general), and each prompt draws one Thompson sample
per candidate and scores it as
quality_weight · sample + cost_weight · normalised_price − tier_distance_penalty · |tier − classified tier|
Port of upstream's adaptive / adaptive_weights / tier_distance_penalty / adaptive_eligible, including
its cold-start phase (unobserved models in the classified tier are tried uniformly first) and its defaults.
{
"adaptive": true,
"adaptiveWeights": { "quality": 0.3, "cost": 0.7 },
"tierDistancePenalty": 0.5,
"adaptiveEligible": "all",
"tiers": {
"SIMPLE": [{ "model": "anthropic/claude-haiku-5-5", "qualityTier": 1, "strengths": ["factual_lookup"] }],
"MEDIUM": ["anthropic/claude-haiku-5-5", "anthropic/claude-sonnet-5-5"],
"COMPLEX": [{ "model": "anthropic/claude-sonnet-5-5", "qualityTier": 2, "strengths": ["code_generation"] }],
"REASONING": { "model": "anthropic/claude-opus-5-5", "thinkingLevel": "high", "qualityTier": 3 }
}
}
adaptiveWeightsmust sum to 1. The upstream complexity-router default leans on cost (0.3 / 0.7).adaptiveEligible: "all"scores every pool model with the distance penalty, so a cheap model with a strong posterior can win aCOMPLEXprompt (a soft floor)."classified_tier"samples only inside the classified tier's pool. The plan-mode floor is always hard: candidates below it are excluded outright.qualityTier(1–3, default 2) andstrengthson a tier entry set the cold-start prior, mirroring upstream'smodel_info.adaptive_router_preferences. Prices come from pi's model registry.- Only the classifier path is adaptive, as upstream:
keywordTierRules, a session-affinity pin and the plan-mode shortcut still take the plain uniform pool pick.
The bandit learns from what happens after each pick, using upstream's signal detectors on pi's agent_end
event: a rephrase or a "forget it" in the next prompt counts against the model that produced the previous
reply; a tool-call loop, a near-duplicate reply, or an errored tool result counts against the model that
produced this one; a "thanks" (after the third turn) counts for it. Posteriors persist in
~/.pi/agent/autorouter-adaptive.json; rows for models no longer in any pool are dropped on load. Inspect
them with /autorouter adaptive, and see how the last pick was scored with /autorouter explain.
Decision log
On by default. Every routing decision prints one line in the shape of upstream's
decision log, so cause= searches work the same
against pi's console and a LiteLLM proxy log:
autorouter: routing decision cause=heuristic_scorer, tier=SIMPLE, score=-0.150, signals=[short (7 tokens), simple (what is)], routed_model=anthropic/claude-haiku-5-5
autorouter: routing decision cause=literal_keyword_match, tier=REASONING, routed_model=anthropic/claude-opus-5-5, thinking=high, keyword="migration"
autorouter: routing decision cause=semantic_keyword_match, tier=REASONING, signals=[semantic_match (0.81 ≈ "kubernetes deployment")], routed_model=anthropic/claude-opus-5-5, thinking=high
autorouter: routing decision cause=llm_classifier, tier=COMPLEX, signals=[llm_classifier], routed_model=anthropic/claude-sonnet-5-5
autorouter: routing decision cause=decision_classifier, tier=COMPLEX, signals=[typesafe-classifier:COMPLEX, typesafe-confidence=0.812000, tier-probability:SIMPLE=0.021000, tier-probability:MEDIUM=0.142000, tier-probability:COMPLEX=0.812000, tier-probability:REASONING=0.025000], routed_model=anthropic/claude-sonnet-5-5
autorouter: routing decision cause=session_affinity_pin, tier=MEDIUM, routed_model=anthropic/claude-sonnet-5-5
autorouter: routing decision cause=question_reply, tier=COMPLEX, signals=[question_reply (ask_user_question)], routed_model=(unchanged)
autorouter: routing decision cause=default_model_fallback, signals=[classifier timed out after 3000ms], routed_model=anthropic/claude-haiku-5-5, thinking=medium
pi-specific fields (thinking, keyword, escalated, plan_floored, fallback, the adaptive phase under
signals) appear only when set, after upstream's fields.
- Interactive mode: the line is drawn dimly in the chat, just below the prompt it routed. It is a rendering
of the decision entry the extension already records in the session, so it costs no tokens, never reaches the
model, and is redrawn when a session is resumed. Press ctrl+o (expand tool output) to swap every line for
the full breakdown
/autorouter explainwould show. **pi -p/ RPC**: the line goes to stderr, keeping stdout clean for the agent's output./autorouter log [n]replays the session's decisions (or the lastn) on demand."decisionLog": falsesilences both; decisions are still recorded and/autorouter logstill works.
Useful commands
| Command | Purpose |
|---|---|
/autorouter |
Show current classifier, last decision, tier, and score |
/autorouter init [provider] [project] [llm] |
Generate a config from the models pi can reach |
/autorouter explain |
Per-dimension breakdown of why a prompt got its tier (and how the bandit scored it) |
/autorouter log [n] |
Replay the session's routing decisions as log lines |
/autorouter adaptive |
The bandit's learned posteriors per request type and model |
/autorouter set [global|project] <setting.path> <JSON value> |
Persist any router setting (project scope by default) |
/autorouter tier <tier> <model> [thinking-level] |
Set a tier's model and optional thinking level |
/autorouter next <model> [thinking-level] |
Force a model and optional thinking level for the next prompt only |
/autorouter pin [model] |
Route every prompt to one model (default: the one serving the session) until unpin |
/autorouter off |
Pause routing for the session: autorouter/tiered keeps the model it is on |
/autorouter on |
Resume routing (clears off, pin, and next) and select autorouter/tiered |
--no-autorouter |
CLI flag to start a session with routing paused |
set accepts dotted config paths and JSON values, so it covers every autorouter.json
setting without restarting Pi. For example:
/autorouter tier REASONING anthropic/claude-opus-5-5 high
/autorouter next anthropic/claude-sonnet-5-5 medium
/autorouter set tierBoundaries.medium 0.35
/autorouter set questionReply.enabled false
/autorouter set keywordTierRules [{"keywords":["migration"],"tier":"REASONING"}]
set writes .pi/autorouter.json by default; prefix it with global to write
~/.pi/agent/autorouter.json. Each save validates the entire configuration and takes effect immediately.
Disabling the extension
Routing only happens while autorouter/tiered is selected, so picking another model in /model is enough to
stop it. /autorouter off pauses routing for the current session without leaving autorouter/tiered, and
--no-autorouter does the same for one launch. To stop the extension from loading at all - for instance
because you'd rather let an external LiteLLM proxy do the routing - use pi's own mechanisms:
Toggle it interactively: run
pi config, find the extension, and disable it. Tab switches between global (~/.pi/agent/settings.json) and project-local (.pi/settings.json) scope;pi config -lstarts in the project scope.Keep the package installed but load nothing from it: use the object form in
settings.json, which filters what a package contributes:{ "packages": [ { "source": "npm:@coresofthq/pi-litellm-autorouter", "extensions": [] } ] }A project
.pi/settings.jsonentry overrides the global one, so you can disable it for a single repo and keep it everywhere else.Remove it entirely:
pi remove npm:@coresofthq/pi-litellm-autorouter.
Your autorouter.json is left untouched by all of these, so re-enabling picks up where you left off.
Development
npm run check # tsc --noEmit && vitest run
npm test # vitest run
Changelog
2.0.0
- Breaking: routing is now a pi virtual model,
autorouter/tiered. Select it in/model, start pi with--model autorouter/tiered, or make it the default model to route; any other model bypasses the router. Requires pi 0.99 or later. - Breaking: the slash command is now
/autorouter(was/autoroute), the flag--no-autorouter, and log lines and notices start withautorouter:. Sessions from 1.x keep their pins, overrides and decision log, and plan-mode extensions can keep emittingautoroute:plan-mode(nowautorouter:plan-mode) - Tool-call continuations, retries, steers and compaction summaries stay on the model serving the turn;
the thinking level selected on
autorouter/tieredis the default for tiers without their own - Add the decision-model classifier (
classifierType: "decision") for Jev, Clef and any other classifier model pi supports classifierContextIncludeAssistantTurnsnow quotes only the assistant's latest text reply, and it no longer takes a context-window slot from your own prompts- Example configs use the latest models, and a new
examples/autorouter.recommended.jsonpairs the mixed-provider tiers with the Jev classifier
1.3.0
- Allow configuring the extension without editing the json file
1.2.1
- Notify when one or several of the configured models are unavailable
1.2.0
- Better support for todo tools. I.e. use the best model based on the todo item content, instead of using the current one
- Improved handling off user question tools. Now it will choose the model based on the context of the task, instead of reclassifying based on an "ok" or "option 2" response.
1.1.0
This version is all about upstream feature parity.
- Decision log: The extension now writes what model was selected and why
- Custom Technical Keywords: Append to the built in technical keywords list
- Adaptive Mode: Pick tier/model from a configured pool
- Semantic Keyword Matching: Use an embedding model to determine keyword matches for tier selection.
1.0.2
- Removed proxy strategy, only local makes sense. if using external LiteLLM, just disable the extension.
1.0.1
- Initial NPM release
Prior art
Ported from BerriAI/litellm (MIT, outside enterprise/) - the v2
router lives in litellm/router_strategy/complexity_router/. This is a port of the algorithms, rubrics, and
config shapes, not a redistribution of LiteLLM code.
License
MIT - see LICENSE.