pi-model-auto
Pi extension package that routes each turn by Low/Medium/High/Ultra capability modes.
Package details
Install pi-model-auto from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-model-auto- Package
pi-model-auto- Version
0.2.2- Published
- Aug 3, 2026
- Downloads
- 1,374/mo · 310/wk
- Author
- maynewong
- License
- MIT
- Types
- extension
- Size
- 224.1 KB
- Dependencies
- 0 dependencies · 2 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-model-auto
pi-model-auto adds one Pi model: Pi Router (Auto).
Choose it once with /model. After that, Pi keeps using the router, and the router chooses one of your authenticated models for each turn.
Start
Install from npm:
pi install npm:pi-model-auto
Or install from git (pin a release with @vX.Y.Z, or omit it to track the default branch):
pi install git:github.com/maynewong/pi-model-auto@v0.1.0
Update later with pi update --extensions.
To try it once without installing, point -e at a local checkout:
pi -e /path/to/pi-model-auto
Then:
- Run
/model. - Choose Pi Router (Auto).
- Use Pi normally.
No config is required at first. If the router says no authenticated models are available, run /login for the providers you want to use, then reload Pi.
Use
Check what the router sees:
/auto
Most turns should use automatic routing. At the start of a new conversation, you can pin the initial capability mode:
@low summarize this file
@medium implement this small change
@high debug this failing test
@ultra investigate this architecture issue
@model:anthropic/claude-3-5-sonnet-20241022 use Sonnet here
@low,@medium,@high, and@ultratarget the matching capability mode.@model:provider/model-iduses that exact model.- Without a prefix,
autorouting lets task difficulty choose the target mode.
These prefixes are intentionally only honored on the first user turn of a conversation. They do not create an isolated subagent or trimmed context. If you used them after a long history, the selected model would need to read the existing session history, which can be much more expensive. Start a new session when you want to pin a mode cleanly.
The prefix is removed before the model sees your prompt. cheap and strong are no longer supported routing hints; use low and ultra instead.
Configure When Needed
Most people only need config for two reasons:
- Limit the pool to providers they trust.
- Tell the router what each model really costs them.
Config lives in either file:
~/.pi/agent/model-router.json.pi/model-router.jsonin trusted projects
Project config overrides user config.
{
"router": {
"modelFilter": { "include": ["anthropic", "z-ai"], "exclude": [] },
"modeModels": {
"ultra": "anthropic/claude-3-5-sonnet-20241022"
},
"modelOverrides": {
"anthropic/claude-3-5-sonnet-20241022": { "costCoef": 0.05 },
"z-ai/glm-5.2": {
"costCoef": 0.4,
"costCoefHours": [{ "hours": [14, 18], "factor": 3 }]
}
}
}
}
Use provider/model ids exactly as they appear in your Pi registry. The names above are public examples. modeModels is optional; if you omit it, the router builds each mode from benchmark metadata.
costCoef
costCoef multiplies the benchmark cost:
< 1: cheaper for you than the benchmark, such as a subscription or discount.= 1: roughly benchmark cost.> 1: more expensive for you.
Avoid setting a limited subscription near zero unless you want it to win almost every turn.
costCoefHours
Use this when a model is more expensive during local hours:
"z-ai/glm-5.2": {
"costCoef": 0.4,
"costCoefHours": [{ "hours": [14, 18], "factor": 3 }]
}
This means 0.4 normally, 1.2 from 14:00 through 17:59. Windows are half-open [start, end). [22, 2] wraps across midnight.
How It Chooses
The router compares quality against your effective cost.
Quality comes from one benchmark table. Cost starts from the same table, then applies your costCoef and any active time window. When the table reports reasoning effort, the router treats (model, effort) as the measured operating point. The router keeps to the efficient frontier: a model-effort variant is only worth considering if no other available variant is both better and cheaper.
capabilitySource chooses the benchmark:
"ramp"(default): this package's Ramp SWE-Bench table, using coding-agent resolve rate and measured cost per task. It is a narrow coding-agent slice, not a general model score. The task family follows SWE-bench."aa": Artificial Analysis model data, using its Intelligence Index and blended price metrics.
The numeric tables live in src/canonical-models.ts. The two sources are not mixed.
The bundled Ramp table keeps every measured row because users rarely have identical model access. Routing first filters that table to the locally available, authenticated, quota-eligible models and recomputes the Pareto frontier with effective costCoef pricing. Models on Ramp's highlighted score-versus-spend wall receive priority only when their marginal capability gain remains within the selected mode's willingness budget; otherwise the router keeps the locally better fallback.
The user-facing status label is a capability mode. Cost is a separate routing axis and never determines this label.
For the Ramp source, the mode is derived only from SWE-bench solve rate:
| mode | Ramp solve rate | representative measured models |
|---|---|---|
Ultra |
>= 85% |
claude-opus-5, claude-fable-5, kimi-k3 |
High |
80–85% |
gpt-5.5, gpt-5.6-sol, grok-4.5, glm-5.2 |
Medium |
75–80% |
deepseek-v4-flash, claude-opus-4-8, gpt-5.6-terra, claude-sonnet-5 |
Low |
< 75% |
gpt-5.4-nano, qwen3.7-plus, gpt-5.6-luna, gemini-3.1-pro |
For the AA source, the same four modes are mapped from Artificial Analysis Intelligence Index bands:
| mode | AA Intelligence Index | representative AA models |
|---|---|---|
Ultra |
>= 56 |
claude-opus-5, claude-fable-5, gpt-5.6-sol, kimi-k3 |
High |
52–56 |
gpt-5.6-terra, grok-4.5, claude-opus-4-7, claude-sonnet-5, gpt-5.5 |
Medium |
> 41 and < 52 |
gpt-5.4, gpt-5.6-luna, glm-5.2, gemini-3.1-pro, kimi-k2.7-code |
Low |
<= 41 |
deepseek-v4-flash, glm-5.1, gpt-5.4-mini, gpt-5.4-nano, gemini-3.1-flash-lite |
Representative models are examples from the bundled benchmark tables, not requirements. The router only uses models that are present in your local Pi registry, authenticated, and allowed by modelFilter. If none of your available models are in the target mode, the router borrows the nearest stronger mode; if no stronger mode exists, it uses the strongest available lower mode. If no authenticated benchmark-backed models are available at all, it asks you to /login or configure model metadata.
Task difficulty chooses the mode, while the continuous difficulty score sets a capability floor inside that mode. The router picks the cheapest effective-cost model meeting the floor, then permits only affordable willingness upgrades inside the same mode. It never enters the next mode early. Models without capability-mode metadata retain Pareto routing.
Task difficulty is judged from language-neutral signals: context size, prompt length, and recent tool activity. In automatic routing, benchmark-backed effort is selected with the model and takes precedence over Pi's session default before the model's thinkingLevelMap is applied. Forced concrete-model routes still honor Pi's selected effort. When you know a task is harder than it looks, start a new session with @high or @ultra.
By default, the router uses only the local heuristic and makes no extra classifier model call. You can opt in to a small current-turn LLM classifier by setting classifierModel.
Classifier and Coarse Auto Detection
Automatic routing has two layers:
- Local heuristic, always available. It computes a rough difficulty score from language-neutral signals: estimated context size, current prompt length, and recent tool-result density. This is free, deterministic, and private, but intentionally coarse; it cannot deeply understand whether a short request is semantically hard.
- Bounded LLM classifier, opt-in. It runs only after you configure
classifierModelwith an exact model id. For auto routing, the router first makes a local draft decision. It runs the classifier for the current turn only when there is no previous route yet, or when that draft decision would switch away from the previous/warm model. Forced@low/@medium/@high/@ultra/@model:...turns and tool-call continuations do not run the classifier.
Local heuristic, no LLM
When the classifier is disabled or skipped, the router computes:
score = normalize(estimatedContextTokens, 8_000, 120_000) * weights.contextTokens
+ normalize(lastUserMessage.length, 120, 1_200) * weights.lastUserLen
+ min(1, recentToolResults / 8) * weights.toolDensity
Default weights are:
{
"weights": {
"contextTokens": 0.3,
"lastUserLen": 0.5,
"toolDensity": 0.2
}
}
The score maps to mode buckets:
| score | mode |
|---|---|
< 0.30 |
low |
0.30–0.52 |
medium |
0.52–0.74 |
high |
>= 0.74 |
ultra |
This is deliberately language-neutral: it does not look for English keywords. It mostly asks, "how much context, how much prompt, how much recent tool activity?" That makes it cheap and predictable, but it can under-classify short, high-risk requests.
LLM classifier prompt
If the bounded classifier runs, it receives the following system prompt:
You are Pi Router's classifier. Classify the current user turn for model routing.
Return exactly one line and no prose: mode=<low|medium|high|ultra> profile=<balanced|coder|deep|fast|vision> score=<0..1>.
Mode is required capability, not cost. Cost is handled later by the router.
Mode rubric:
- low: trivial, explain, summarize, docs/copy edits, tiny localized change, low ambiguity.
- medium: routine coding/debugging/refactor, small scoped implementation, ordinary tests, a few files.
- high: multi-file work, failing tests, unknown root cause, API/design decisions, non-trivial tool use, sizeable context.
- ultra: architecture/security/migrations, production-risk changes, large refactor, long-horizon investigation, high ambiguity or high cost of failure.
Profile rubric:
- vision if hasImage=true.
- fast only when the user explicitly prioritizes speed/latency or the task is a simple transform.
- coder for implementation, debugging, refactoring, tests, or code review.
- deep for architecture, root-cause analysis, security, planning, or long-horizon reasoning.
- balanced otherwise.
Score is position inside the chosen mode: low 0.00-0.29, medium 0.30-0.51, high 0.52-0.73, ultra 0.74-1.00.
If uncertain between adjacent modes, choose the lower mode unless the task is risky, irreversible, security-sensitive, or explicitly asks for deep investigation.
The classifier model receives only this user payload:
estimatedContextTokens=<local estimate>
hasImage=<true|false>
lastUserMessage:
<the latest user message>
It does not receive the full conversation history. The full session is only inspected locally to estimate token count, count recent tool results, and detect whether images exist.
The classifier returns the same low / medium / high / ultra vocabulary users see in the UI. The optional score positions the request inside that mode's capability floor; the classifier does not directly choose a model.
Classifier cost controls
Classifier cost is bounded in several ways:
- The classifier prompt omits prior assistant/user turns and tool output bodies.
- Output is capped at
maxTokens: 80. - It uses
temperature: 0,reasoning: minimalwhen the model supports reasoning, the configured timeout, and no internal retries. - It is gated by the local draft decision: after the first auto turn, if the draft keeps the previous/warm model, no classifier call is made.
- Tool-call continuations reuse the same turn route and do not run extra classifier calls.
- Forced first-turn pins (
@low/@medium/@high/@ultra/@model:...) do not run the classifier. - If the classifier output cannot be parsed or the model fails repeatedly, that classifier model is temporarily disabled and routing falls back to the local heuristic / other classifier candidates.
Choosing whether to use the classifier
The classifier is off by default. To enable semantic classification, pin it to an exact endpoint:
{
"router": {
"classifierModel": "gateway/gpt-5.4-nano"
}
}
Use the exact provider/model id from your Pi registry. If you want a Low-mode classifier, pick a model that is actually available locally and falls in Low for your active capabilitySource; for example, with the default Ramp source, gpt-5.4-nano, qwen3.7-plus, or another local Low model can be good candidates. classifierModel is an exact pin, not a mode name: use "gateway/gpt-5.4-nano", not "low".
You can still force it off explicitly, even if a shared config sets a classifier model:
{
"router": {
"classifier": "off"
}
}
Trade-offs:
- Default / classifier off: no extra model call; routing relies only on local context size, prompt length, and recent tool activity.
- Pinned low-cost classifier: predictable and cheap, but may miss subtle hard requests.
- Pinned stronger classifier: may classify hard/ambiguous work better, but adds cost and is usually unnecessary because the main route still has local heuristics, manual first-turn mode pins, and cache-aware routing.
One user turn keeps one model, including tool-call continuations. Automatic routing also avoids quota-cooled plans and avoids switching away from a useful warm cache when the switch is not worth it.
Context Window and Compaction
Pi displays context usage and decides when to compact from the selected pi-router/auto model. The router keeps that virtual model's contextWindow synchronized with the concrete model selected for the current turn. It preselects the route before Pi's preflight compaction check, refreshes the value again when the provider request starts, and restores the most recent concrete model's window when resuming a session.
As a result, a turn routed to a 272K model displays and compacts against 272K; a later turn routed to a 1M model switches the same limits to 1M. The initial 1M registration is only a startup placeholder before a session target can be identified.
Settings
| setting | use |
|---|---|
capabilitySource |
Choose "ramp" or "aa". |
modelFilter |
Include or exclude providers/models by substring. |
modeModels |
Pin exact endpoints for low, medium, high, or ultra. |
modelOverrides |
Adjust cost or metadata for known/private/local models. |
willingness |
Control affordable same-mode upgrades after a capability mode is selected. |
cacheAware |
Keep warm prompt caches when switching is not worth it. Enabled by default. |
quota |
Skip cooled-down plans after rate-limit headers or 429. Enabled by default. |
classifier |
Tune or explicitly disable the bounded LLM classifier. Use "off" to disable. |
classifierModel |
Opt in to the classifier by pinning a specific provider/model or variant in your pool. Use an exact id, not low/medium/high/ultra. |
weights |
Language-neutral difficulty-scoring weights. Advanced. |
log |
Append routing decisions to .pi/router.log. |
Useful override fields:
| field | meaning |
|---|---|
costCoef |
Cost multiplier. |
costCoefHours |
Local-hour multipliers. |
canonical |
Name shown in /auto. |
costTier |
Cost-only classification: cheap, standard, premium, or unknown. |
capabilityMode |
Override the capability mode: low, medium, high, or ultra. |
profiles |
deep, fast, coder, balanced, vision, frontier. |
frontier |
Whether the model can appear in the frontier display/pool. |
benchmarkEffort |
Pi-normalized effort (minimal, low, medium, high, xhigh) backing a manual metric row. |
priceBlended, intelligence, scores, tps |
Raw metrics for models without benchmark data. |
Quota state is stored at ~/.pi/agent/quota-state.json. Providers without remaining-quota headers only cool down after a real 429.
Core API
Other Pi extensions can resolve a model without an ExtensionContext:
import { resolveRouteModel } from "pi-model-auto/core";
const selection = resolveRouteModel({
models: availableModels,
hint: "high", // routing hint: low | medium | high | ultra | auto | provider/model
context,
});
The core API loads user-level model-router.json and quota state by default. It never reads project config because that requires a trust decision from the host. Pass cfg to supply an explicit configuration or filterQuota: false to disable persisted cooldown filtering.
Develop
npm run build
npm run typecheck
npm test
Maintainers: see RELEASING.md for the publish flow.