@bramburn/pi-model-council
Pi extension: multi-model coding decisions via OpenRouter
Package details
Install @bramburn/pi-model-council from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@bramburn/pi-model-council- Package
@bramburn/pi-model-council- Version
1.6.11- Published
- Jun 30, 2026
- Downloads
- 88/mo · 88/wk
- Author
- bramburn
- License
- unknown
- Types
- extension
- Size
- 184.6 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
model-council
Pi extension for multi-model coding decisions via OpenRouter.
Ask three independent AI models for a second opinion, then have a fourth model synthesise them into a single actionable plan. Use the fast /opinion command for quick checks, and /council for higher-stakes architectural decisions.
Features
- Multi-model council — Three OpenRouter models deliberate independently, then a fourth model (you pick) synthesises the final decision. Returned as a structured plan you (or Pi) can implement.
- Single-model opinions — Get a quick second opinion from any model with valid auth, using Pi's built-in model registry.
- Pi-native auth — Re-uses
OPENROUTER_API_KEY(or/login openrouter) so the API key lives in one place. No separate key prompt required when Pi already knows OpenRouter. - Persistent settings — Stored in
~/.pi/agent/council-settings.json(or project-scoped when trusted). Includes the three council members, the synthesis model, and the opinion model. - Secure — Settings are gitignored; keys never logged.
Installation
pi install accepts three source types: npm, git, and local path. Pick the one that matches how you want to consume the extension.
Prerequisites
- Pi installed (
pi --versionshould print ≥ 1.0) - Node.js 22+ (matches the extension's CI runner)
- An OpenRouter API key — get one at openrouter.ai/keys
Pick the install method
| Use case | Command | Where it lands |
|---|---|---|
| npm (recommended once published) | pi install npm:pi-model-council |
~/.pi/agent/npm/pi-model-council |
| GitHub via SSH (most common) | pi install git:github.com/bramburn/pi-model-council |
~/.pi/agent/git/github.com/bramburn/pi-model-council |
| GitHub via HTTPS + PAT (private repos, CI) | pi install https://x-access-token:<TOKEN>@github.com/bramburn/pi-model-council.git |
~/.pi/agent/git/github.com/bramburn/pi-model-council |
| Local clone (development) | pi install ./pi-model-council |
registered in-place, not copied |
| Try without installing (one-shot) | pi -e git:github.com/bramburn/pi-model-council |
temp directory |
Pin to a specific tag for reproducibility (recommended for shared / CI use):
pi install git:github.com/bramburn/pi-model-council@v1.5.0
Detailed walkthroughs
Option 1 — npm (once the package is published)
pi install npm:pi-model-council
Updates use the regular npm flow: pi update --extensions.
Option 2 — GitHub via SSH
If you've already added an SSH key to your GitHub account (the same key you'd use for git push to this repo), Pi uses it automatically:
pi install git:github.com/bramburn/pi-model-council
# Or pinned to a tag:
pi install git:github.com/bramburn/pi-model-council@v1.5.0
The clone is placed under ~/.pi/agent/git/github.com/bramburn/pi-model-council/.
Option 3 — GitHub via HTTPS with a Personal Access Token
Use this when SSH isn't an option. Create a fine-grained token at https://github.com/settings/tokens?type=beta with read access scoped to just bramburn/pi-model-council:
pi install https://x-access-token:<TOKEN>@github.com/bramburn/pi-model-council.git
The PAT will be embedded in
~/.pi/agent/settings.json. The settings file is created with0600permissions, but treat it as a secret and don't commit it.
Option 4 — Local clone (recommended for development)
# Clone once anywhere
git clone https://github.com/bramburn/pi-model-council.git ~/code/pi-model-council
cd ~/code/pi-model-council
npm install
# Register the local path with pi (no copy — edits take effect immediately)
pi install ./pi-model-council
To update later:
cd ~/code/pi-model-council
git pull && npm install
# The next pi launch picks up the new code automatically; no reinstall needed.
# If you changed pi-extension metadata, force a refresh:
pi update --self --force
Option 5 — Try without installing (one-shot smoke test)
pi -e git:github.com/bramburn/pi-model-council
Pi clones to a temp directory and uses the extension for the current run only. Nothing is written to your settings.
Project-scoped install (team sharing)
By default, pi install writes to user settings (~/.pi/agent/settings.json). To write to project settings (.pi/settings.json) so the install is checked into your team's repo and auto-applied when a teammate opens the project:
pi install -l git:github.com/bramburn/pi-model-council@v1.5.0
After this, commit .pi/settings.json to your project. When a teammate clones and Pi trusts the project, the extension installs automatically.
Verify the install
pi list # shows installed packages and their sources
pi config # toggle the extension on/off
You should see pi-model-council listed and enabled. Then launch Pi and try:
pi
> /council-settings
If the council UI opens, the extension is loaded correctly.
Updating an installed version
# Update every installed extension to the ref pinned in settings
pi update --extensions
# Update only this one to a new tag/commit
pi install git:github.com/bramburn/pi-model-council@v1.6.0 # overwrites
Uninstalling
pi remove pi-model-council
Or manually delete the entry from ~/.pi/agent/settings.json and remove the clone under ~/.pi/agent/git/github.com/bramburn/pi-model-council/. Your settings file (council-settings.json) is kept — re-installing will pick them back up.
Workflow: Getting Started in 3 Steps
The whole setup takes about a minute. Follow this order — each step builds on the previous one.
Step 1 — Authenticate with OpenRouter (one-time)
Pi already ships with an OpenRouter provider. Give it your API key once and Pi will list every supported OpenRouter model under provider === "openrouter".
# Option A — environment variable (recommended for CI / dotfiles)
export OPENROUTER_API_KEY="sk-or-v1-..."
# Option B — interactive login (stores in ~/.pi/agent/auth.json with 0600 perms)
# Inside Pi, run:
/login openrouter
Get a key from openrouter.ai/keys.
Verify Pi sees the models:
pi --list-models
# Look for entries like:
# openrouter/anthropic/claude-3.5-sonnet
# openrouter/openai/gpt-4o
# openrouter/qwen/qwen3.7-max
Step 2 — Configure the extension
Run the council settings UI. Pi will detect OpenRouter in your model registry and skip the API-key prompt entirely.
/council-settings
The UI uses a typeahead-searchable, scrollable picker (same UX as /model), so you can filter through hundreds of OpenRouter models by typing a few characters — fuzzy-matched across model name and id. Up/Down to navigate, Enter to select, Esc to cancel.
The UI walks you through:
- Council Model 1 of 3 — first dissenting voice
- Council Model 2 of 3 — second dissenting voice (already-picked models are filtered out)
- Council Model 3 of 3 — third dissenting voice
- Synthesis Model — reads all three opinions and writes the final plan. Defaults to "Council Model 1" since you already trust it; pick any OpenRouter model you like.
- Second Opinion Model — the model used by
/opinionfor quick checks - Structured Output — JSON schema for faster parsing (recommended: yes)
If Pi doesn't see OpenRouter yet (because you skipped Step 1), the UI falls back to a manual flow: it asks for an API key, pings OpenRouter to verify, fetches the live model list, and proceeds the same way.
Re-run anytime to swap models:
/council-settings list # show current config
/council-settings reset # wipe and reconfigure
For just the opinion model:
/opinion-settings
Step 3 — Use the council
# Three deliberating models + one synthesis model
/council fix "The login fails on mobile devices"
/council ask "Should we use hooks or context for state?"
/council architecture "Where should auth state live?"
# Single-model quick check (uses your /opinion model)
/opinion "How should I refactor this function?"
/opinion fix "Lesson progress resets after navigation"
The full report is saved to .pi/council/last-decision.md (and .pi/council/last-opinion.md for /opinion). The Pi agent can read this file when continuing the conversation.
How Model Selection Works
The extension uses two layers:
| Layer | Source | Purpose |
|---|---|---|
| Discovery | ctx.modelRegistry.getAvailable() |
Lists every OpenRouter model Pi knows about (the curated list ships with Pi). Filtered to provider === "openrouter". |
| Validation | Same registry, then OpenRouter /models fallback |
Confirms each chosen model ID still exists at runtime. |
| Inference | Direct OpenRouter REST call (openrouterClient.ts) |
Sends the council prompts to each model and the synthesis prompt to the chosen synthesiser. |
This means:
- If Pi already knows about OpenRouter (Step 1 above),
ctx.modelRegistry.getAvailable()provides the model list — no extra HTTP call. - If the registry is empty (e.g. fresh install, no API key yet), the UI prompts for a key and fetches the live model list directly from OpenRouter.
- The chosen model IDs are stored in
council-settings.jsonand used to make the actual inference calls. They are not changed when you swap Pi providers.
Why a separate Synthesis Model?
The three council members argue from different angles; the synthesis model is the judge that weighs them and produces a single plan. By default it's "Council Model 1" because you already trust that model. For higher-stakes decisions, pick a model known for careful reasoning (e.g. anthropic/claude-3.5-sonnet, openai/o1) — it's a 4th API call on top of the three council calls.
The 4-Model Orchestration Flow
When you run /council, a single council_decide tool call internally fans out to four models. Pi sees only the final synthesised report — all sub-calls are hidden behind one logical tool:
/council fix "..." (slash command)
│
▼
┌───────────────────────────────┐
│ council_decide tool │ (single Pi tool call)
│ councilRunner.ts │
└───────────────────────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Model 1 │ │ Model 2 │ │ Model 3 │ ← Promise.all
│ (council)│ │ (council)│ │ (council)│ parallel
└────┬─────┘ └────┬─────┘ └────┬─────┘ fan-out
│ │ │
▼ ▼ ▼
opinion A opinion B opinion C (JSON)
│ │ │
└──────────────────┼──────────────────┘
▼
┌──────────────┐
│ Model 4 │ ← synthesis step
│ (synthesis) │ reads 3 opinions
└──────┬───────┘ writes 1 decision
▼
┌──────────────┐
│ Pi receives │ ← tool result returned to agent
│ Markdown │
│ report │
└──────────────┘
Key properties:
- All four calls happen inside one tool execution, so the agent sees a single tool call in its reasoning graph — no multi-turn coordination overhead.
- The three council calls run in parallel via
Promise.all. Each call has its ownwithTimeout+retrywrapper, so a slow or failing model doesn't block the others. - If 1 of 3 council models fails, the synthesis step still proceeds with the 2 successful opinions. If all 3 fail, the runner surfaces a clear error.
- The synthesis call has a longer timeout (
synthesisTimeoutMs) because the synthesis prompt includes all three opinions. ctx.signalis threaded through every downstreamfetch(), so pressing Esc during a long council cancels all in-flight calls (Pi'swithTimeouthelper combines the parent signal with per-call timeouts).
Where the API Key Comes From
Resolution order:
council-settings.json→openRouter.apiKey(the legacy explicit-prompt flow)- Pi's auth storage →
modelRegistry.getApiKeyForProvider("openrouter")(when you've setOPENROUTER_API_KEYor run/login openrouter) process.env.OPENROUTER_API_KEY
If none of these resolve, /council and /opinion both fail fast with a setup error pointing you at /council-settings.
Commands
/council
Three OpenRouter models + one synthesis model.
/council fix "The login fails on mobile devices"
/council ask "Should we use hooks or context for state?"
/council architecture "Where should auth state live?"
Modes:
fix— debug a known problemask— open technical questionarchitecture— design-level decision- (default) — generic second opinion
/opinion
Single-model quick check.
/opinion "How should I refactor this function?"
/opinion fix "The lesson progress resets after navigation"
Modes: fix, ask, architecture, general.
/council-settings
Configure the OpenRouter setup.
/council-settings # Open the settings UI (3 council + 1 synthesis + opinion)
/council-settings list # Show current settings (API key redacted)
/council-settings reset # Reset all council settings
/opinion-settings
Configure just the /opinion model.
/opinion-settings # Open the settings UI
/opinion-settings list # Show current model
/opinion-settings reset # Reset to default
Models
Council (3 Members)
The three models you pick in /council-settings for independent deliberation. Recommended starting set (all available on OpenRouter):
anthropic/claude-3.5-sonnet— careful reasoning, code qualityopenai/gpt-4o— broad knowledge, multimodalqwen/qwen3.7-max— strong code model, cost-effective
Pick models that disagree productively. If they all share a training cutoff, you get correlated blind spots.
Synthesis Model
The fourth model that reads the three opinions and writes the single recommendation. Defaults to "Council Model 1". For high-stakes decisions, consider a reasoning-tuned model:
openai/o1— strong step-by-step reasoninganthropic/claude-3.5-sonnet— careful, balanceddeepseek/deepseek-r1— reasoning model with low cost
Second Opinion (/opinion)
Any model with valid auth works. Pi's modelRegistry.getAvailable() is the source of truth, so any provider you've configured (Anthropic, OpenAI, Google, OpenRouter, etc.) is selectable.
Architecture
pi-model-council/
├── index.ts # Extension entry point
├── councilRunner.ts # 3-model + synthesis logic
├── secondOpinionRunner.ts # Single-model logic
├── settings.ts # Settings persistence
├── settings-ui.ts # Settings TUI components (registry-aware)
├── openrouterClient.ts # OpenRouter REST client + JSON repair
├── prompts.ts # Proposal + synthesis prompts (blind-label)
├── structuredOutput.ts # JSON schemas + repair
├── markdown.ts # Decision report formatter
├── searchSelector.ts # Searchable model picker (custom TUI)
├── retry.ts # Timeout + retry helpers
├── schemas.ts # TypeBox tool parameter schemas
├── commandParser.ts # CLI argument parsers
└── types.ts # Shared TypeScript types
Development
# Install dependencies
npm install
# Type check
npm run typecheck
# Lint
npm run lint
# Run tests
npm test
# Watch mode
npm run test:watch
# Coverage
npm run test:coverage
# Audit dependencies
npm run audit
Security
See SECURITY.md for the security policy and reporting vulnerabilities.
API keys are stored in council-settings.json (when set explicitly) or in Pi's ~/.pi/agent/auth.json (when set via env var or /login). Both files use 0600 permissions and are gitignored. The extension never logs keys, but does echo a redacted preview when listing settings.
Disclaimer
This extension sends your prompts to OpenRouter and the model providers behind it. AI-generated suggestions can be wrong, outdated, or unsafe — always review before applying. See DISCLAIMER.md for the full text.
Contributing
See CONTRIBUTING.md for setup, code style, and the PR process.
Code of Conduct
By participating, you agree to the Contributor Covenant in CODE_OF_CONDUCT.md.
Support
Open an issue using the templates in .github/ISSUE_TEMPLATE/. See SUPPORT.md for where to ask questions.
License
MIT — see LICENSE.