pi-model-sort0.3.15
Sort models in pi's /model selector by last usage — most recently used models appear first
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
🔄 pi-model-sort
Sort models by last usage in pi
Your most-used models appear first — no more scrolling past providers you never touch.
The Problem
Pi's /model selector sorts models alphabetically by provider. If you have Anthropic + OpenAI + Google + Ollama all configured, your most-used model might be buried behind twenty other models. Every time you open the picker, you scroll past providers you haven't touched in weeks. There's no built-in way to say "show me what I actually use."
The Solution
pi-model-sort tracks every model selection and reorders the /model picker so your most recently used models appear at the top.
- Automatic tracking — every
/modelswitch,Ctrl+Pcycle, and session restore is recorded with a Unix timestamp - Sort order — current model first → most recently used descending → provider/id alphabetical fallback
- MRU on interactive startup — unspecified TUI sessions start on your most recently used authenticated model within the configured scope. Explicit model/provider/scope/thinking and restored-session selections always win.
- Per-model thinking levels — remembers the thinking level you last used on each model and restores it on every switch (
/model,Ctrl+P) and on unspecified interactive startup, clamped to what each model supports - Persistent — usage data lives in
~/.pi/agent/extensions/pi-model-sort.json, survives restarts - No config needed — install and forget; the extension starts tracking on first use
- Zero setup — with no recorded usage, models fall back to the default alphabetical order
- Interactive pickers — the sort applies to
/model(Ctrl+L), both "Scope: all" and "Scope: scoped" views, and the/scoped-modelsconfig selector
No settings.json modifications. No manual maintenance. No database.
Usage
The extension works automatically — there are no commands to learn.
# Install, then just use pi normally
/model # Most recently used models appear at the top
Ctrl+P / Ctrl+Shift+P # Cycle through models in last-used order
Open /model and press Tab to switch between "Scope: all" and "Scope: scoped" — both views are sorted by recency.
Model selection authority
MRU selection is an interactive default, not permission to replace a caller's model. Explicit --model, --provider, --models, --thinking, and session restore/continue/fork flags suppress startup MRU selection and the startup thinking restore (including --flag=value forms). Resume, reload, and fork lifecycle events never select MRU or replace the session's thinking level. Candidates must remain within the session's configured model scope.
RPC, print, JSON, SDK/non-TUI sessions, and Fabric workers do not apply MRU model selection or remembered thinking levels, patch their registries, or write interactive usage history. In particular, ctx.hasUI is not an interactive-mode test: Pi RPC also exposes UI methods. Interactive sessions keep picker sorting and per-model thinking memory.
Config File
The extension creates ~/.pi/agent/extensions/pi-model-sort.json automatically on first model switch:
{
"lastUsed": {
"anthropic/claude-sonnet-4-20250514": 1717000000000,
"openai/gpt-4o": 1716995000000,
"google/gemini-2.5-pro": 1716000000000
},
"thinking": {
"deepseek/deepseek-v4-flash": "max",
"anthropic/claude-sonnet-4-20250514": "high"
}
}
No manual editing needed. To clear usage history, delete the file and /reload.
Per-Model Thinking Levels
When you switch models, Pi picks the new model's level from its settings — modelThinkingLevels["provider/model"], else defaultThinkingLevel, else the current level carried over — and clamps it to the model. Those are configured defaults, not the level you last used on that model. This extension learns your per-model preference automatically:
- Change the level with
Shift+Tabor/thinking— it's recorded for the active model - Switch models — the model's last-used level is restored (including when cycling with
Ctrl+P) - Start pi or
/newwithout model/thinking flags — the startup model's last-used level is restored, even when Pi already picked your most recently used model - The restored level is clamped to what the model supports, so a
maxremembered for one model can never push a claude model pasthigh
Example: deepseek-v4-flash stays on max, luna on xhigh, claude models on high — set each once and every switch lands on the right level.
Interaction with Pi's thinking settings
Pi applies its own level first; a remembered level for that model then replaces it. With no remembered level, Pi's level stays in effect.
| Situation | Effective thinking level |
|---|---|
--thinking, --model (including <pattern>:<level>), --provider or --models at startup |
Pi's, from the flags or settings (no startup restore) |
| Resumed, continued, forked or reloaded session | The level saved in that session |
Unspecified interactive startup or /new |
Remembered level, else Pi's startup level |
/model or Ctrl+P switch |
Remembered level, else Pi's switch level |
| RPC, print, JSON, SDK, Fabric workers | Pi's — the extension does not touch thinking |
A remembered level wins over defaultThinkingLevel, a matching modelThinkingLevels entry and a :level suffix in enabledModels/--models scope. For example, if modelThinkingLevels["github-copilot/gpt-6-astra"] is "medium" but pi-model-sort.json remembers "high", switching to Astra lands on high.
| Setting | Stored in | Written by |
|---|---|---|
defaultThinkingLevel, modelThinkingLevels |
Pi's settings.json (~/.pi/agent/ or project .pi/) |
You, /thinking + Ctrl+S, /settings |
thinking["provider/model"] |
~/.pi/agent/extensions/pi-model-sort.json |
This extension, whenever a model's level changes or you leave a model |
Changing the active model's level in /settings also updates its remembered level. To fall back to Pi's settings for a model, delete its entry from thinking and /reload; the entry is learned again from the level you leave that model on.
Install
With pi install (recommended):
pi install npm:pi-model-sort
Or install from GitHub:
pi install https://github.com/monotykamary/pi-model-sort
With npm:
npm install pi-model-sort
Or in ~/.pi/agent/settings.json:
{
"packages": [
"git:github.com/monotykamary/pi-model-sort"
]
}
Then /reload or restart pi.
For quick one-off tests:
pi -e ./model-sort.ts
How It Works
model_select event fires
→ Extension records timestamp
→ Writes to pi-model-sort.json
→ Next /model opens with updated sort
Session starts (startup / new)
→ Extension reads pi-model-sort.json
→ Monkey-patches ModelSelectorComponent.prototype:
sortModels — sorts "Scope: all" view
loadModelsFromSnapshot — sorts "Scope: scoped" scopedModelItems after load
(loadModels on pi <= 0.80.3; renamed/split in 0.80.8)
→ Patches the interactive ModelRuntime getModels/getAvailableSnapshot/getAvailable (legacy ModelRegistry fallback)
→ Monkey-patches AgentSession.prototype._cycleScopedModel
→ Sort order: current model first → most recent → provider/id alphabetical
→ Patches survive modelRegistry.refresh()
→ Overrides initial model to MRU via pi.setModel()
if pi core chose a different model (scopedModels[0], defaultModelPerProvider)
only on unspecified TUI startup/new sessions, within the configured scope
never for explicit CLI selection, resumed sessions, RPC/print/JSON/SDK, or Fabric workers
→ Otherwise restores the startup model's remembered thinking level via pi.setThinkingLevel()
(Pi emits no model_select for its initial model; same conditions as the MRU override)
Five patches + MRU startup override, full coverage:
| Patch | What it affects |
|---|---|
ModelSelectorComponent.prototype.sortModels |
/model TUI picker — "Scope: all" view |
ModelSelectorComponent.prototype.loadModelsFromSnapshot (or loadModels on older pi) |
/model TUI picker — "Scope: scoped" view (configured cycling models) |
AgentSession.prototype._cycleScopedModel |
Ctrl+P / Ctrl+Shift+P cycling order (non-destructive swap, cycling does not update last-used to avoid feedback loop) |
ModelRegistry.getAvailable() |
Interactive /scoped-models config selector |
ModelRegistry.getAll() |
Interactive catalogue enumeration |
pi.setModel() on session_start |
MRU default only for unspecified interactive startup and /new |
pi.setThinkingLevel() on session_start |
Remembered thinking level when the startup model is kept (same conditions) |
When no scoped models are configured, Ctrl+P falls through to _cycleAvailableModel which calls getAvailable() — already sorted by the registry patch.
Why cycling doesn't update last-used: Updating timestamps during Ctrl+P cycling creates a feedback loop — each cycle makes the selected model most-recent, re-sorts it to position 0, and
(currentIndex + 1) % lenalways lands on the second model. Models would toggle forever between the top 2. Manual model selection (/model, session restore) still updates last-used.
The SDK doesn't expose a sort order for model lists. Monkey-patching the component and registry methods is the only way to control ordering without rebuilding the entire picker UI.
The patches survive modelRegistry.refresh() because they wrap the original methods. On reload, the extension detects the prototypes are already patched and just updates the last-used data source.
Comparison with Alternatives
| Approach | Pros | Cons |
|---|---|---|
| pi-model-sort (this) | Automatic, zero-config, persistent, applies everywhere | Monkey-patches internal prototypes |
enabledModels in settings.json (manual) |
Built-in, no extension needed | Allowlist — must list models manually; doesn't sort, just scopes |
Custom /model replacement extension |
Full control over UI | Rebuilds the entire picker component from scratch (~400 lines of TUI code) |
Manually ordering models.json |
Controls --list-models output |
Static, doesn't react to actual usage; doesn't affect /model picker |
Development
npm install
npm test # Vitest unit tests
npm run typecheck # TypeScript validation
npm run lint:dead # Dead code detection (knip)
Structure
.
├── model-sort.ts # Main extension
├── src/
│ └── index.ts # Sort logic, types, and utilities
├── __tests__/
│ └── sort.test.ts # Unit tests for sortByLastUsed
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── knip.json
License
MIT
Pi 1.0 compatibility (0.3.11)
Tested against Pi 1.3.0 with exact SDK development pins and wildcard host peers. Native runtime availability, selector sorting, scoped cycling and prototype cleanup are verified.
Run bun run test:host for the offline real-host regression. Set PI1_HOST_PACKAGE to an installed Pi package directory and PI1_HOST_ENTRY=bundle to test its bundled CLI runtime.
Pi 0.99 compatibility (0.3.10)
Sorts the actual 0.99 ModelRuntime availability paths, including unscoped cycling. Scoped cycling forwards native options and restores temporary order before asynchronous model-selection handlers. Scoped selector patches compose with provider filtering.
Tested with Pi 0.99.0. Host-provided Pi packages are wildcard peers, not bundled dependencies; development uses exact 0.99.0 versions.