pi-9router
v0.1.2
Published
Pi Coding Agent extension for 9router with dynamic model discovery and local cache.
Maintainers
Readme
pi-9router
Pi Coding Agent extension for 9router: register 9router as a Pi provider, discover models dynamically, enrich model metadata, and manage configuration from Pi with /9router.
Repository: https://github.com/ricatix/pi-9router npm package: https://www.npmjs.com/package/pi-9router
Why this exists
9router exposes an OpenAI-compatible API in front of many upstream models. This extension makes Pi see 9router as a native provider:
Pi Coding Agent ── OpenAI-compatible API ── 9router ── upstream modelsYou get one provider in Pi, backed by whatever models/routes your 9router instance exposes.
Features
- Pi provider registration — registers
9routerwith Pi throughregisterProvider. - Dynamic model discovery — fetches available models from
GET /v1/models. - OpenAI-compatible chat — uses Pi's
openai-completionsprovider mode. - Model metadata enrichment — supplements discovered models with
models.devmetadata when available. - Local model cache — starts from cache when possible, then refreshes in background.
- Graceful startup — Pi remains usable even when 9router is not configured yet.
- Dual env var support — accepts both
NINE_ROUTER_*and9ROUTER_*names. - Interactive setup — configure from Pi using
/9router setup. - Slash command management — status, refresh, model listing, reasoning toggle, config clear.
- Secret-safe output — API keys are not printed in status/error output.
Web search/fetch tools are intentionally not registered yet. The extension only exposes ninerouter_status until a stable 9router web-route contract is available.
Requirements
- Pi Coding Agent
- Bun for local development/build
- A 9router API key and reachable 9router base URL
Default base URL:
https://9router.com/v1Installation
From npm
pi install npm:pi-9routerIf your Pi version uses ~/.pi/agent/settings.json package entries, add:
{
"packages": ["npm:pi-9router"]
}From GitHub
pi install git:github.com/ricatix/pi-9routerOr in ~/.pi/agent/settings.json:
{
"packages": ["git:github.com/ricatix/pi-9router"]
}Local checkout
git clone https://github.com/ricatix/pi-9router.git
cd pi-9router
bun install
bun run buildThen add local path to Pi settings:
{
"packages": ["/absolute/path/to/pi-9router"]
}Example:
{
"packages": ["/Users/ricoaditya/Project/open-source-project/pi-9router"]
}Restart Pi after changing packages.
Note: Pi's npm/git package loader discovers extensions through the
pi.extensionsmanifest inpackage.json. This package declarespi.extensions: ["./dist/index.js"], so no extra wiring is required — Pi will import the built entry-point on startup. Local-path installs also work, but rely on a different fallback path in the host.
Configuration
You can configure 9router by environment variables or by /9router setup.
Environment variables
Recommended variable names:
export NINE_ROUTER_API_KEY="your-api-key"
export NINE_ROUTER_BASE_URL="https://9router.com/v1"Also supported:
export 9ROUTER_API_KEY="your-api-key"
export 9ROUTER_BASE_URL="https://9router.com/v1"NINE_ROUTER_* is safer in shells because variable names beginning with digits can be awkward or invalid in some environments.
Config files
Persistent config:
~/.pi/agent/9router-config.jsonModel cache:
~/.pi/agent/9router-models-cache.jsonFiles containing secrets are written with restricted permissions when possible.
URL normalization
NINE_ROUTER_BASE_URL can be provided with or without /v1:
https://9router.com
https://9router.com/v1Both normalize to:
https://9router.com/v1Protocol is required. Use https://... or http://....
Usage
Start Pi after installing/configuring the extension:
piRun status:
/9router statusRefresh discovered models:
/9router refreshList cached models:
/9router modelsOpen interactive setup:
/9router setupAfter discovery succeeds, select a 9router model from Pi's model picker. Models are registered under provider 9router.
/9router commands
| Command | Description |
| --- | --- |
| /9router | Opens interactive setup when no sub-command is provided. |
| /9router setup | Prompts for base URL and API key in Pi TUI. |
| /9router status | Shows base URL, whether API key is set, cache count, and visible registry count when available. |
| /9router refresh | Fetches latest models from 9router, enriches metadata, updates cache, and re-registers provider. |
| /9router reload | Alias for refresh. |
| /9router models | Prints cached model IDs. |
| /9router reasoning on | Stores reasoning toggle as enabled. |
| /9router reasoning off | Stores reasoning toggle as disabled. |
| /9router clear | Clears local config and model cache. |
| /9router debug | Alias for status. |
| /9router config | Alias for setup. |
Note: reasoning config is persisted for future compatibility. Current provider model registration keeps reasoning disabled unless future 9router/Pi contracts expose a stable thinking mapping.
LLM tools
This extension registers one Pi tool:
| Tool | Description |
| --- | --- |
| ninerouter_status | Returns base URL, API-key presence, and cache count. |
Web search/fetch tools are not exposed in this release.
How startup works
- Read env vars and saved config.
- Normalize base URL.
- Register cached provider immediately if model cache exists.
- Refresh models in background when cache exists.
- On first run with no cache, wait for discovery so Pi can list models.
- If discovery fails but cache exists, keep cached models registered.
- If no API key exists, Pi still loads and
/9router setupremains available.
Troubleshooting
/9router command is not registered
Pi's package loader only auto-loads an extension entry-point if the package
declares it through the pi.extensions field in package.json. This package
ships with:
{
"pi": {
"extensions": ["./dist/index.js"]
}
}If /9router is missing after pi install npm:pi-9router:
- Verify the installed
package.jsonhas thepi.extensionsfield:cat ~/.pi/agent/npm/node_modules/pi-9router/package.json | grep -A2 '"pi"' - If absent, your installed version is older than
0.1.2. Reinstall:pi install npm:pi-9router - Restart Pi (or run
/reload) so the new manifest is picked up. - Confirm the package is loaded:
pi listnpm:pi-9routershould appear under User packages.
Local-path installs (local:/path/to/pi-9router) hide this issue because the
host falls back to importing dist/index.js even without the manifest — but
the manifest is still required for npm: and git: sources.
API key belum ada
Set API key using env vars or run:
/9router setupNo 9router models in Pi
Run:
/9router status
/9router refreshCheck:
NINE_ROUTER_API_KEYis set.NINE_ROUTER_BASE_URLpoints to a reachable 9router endpoint.GET <baseUrl>/modelsreturns model data.
For default hosted 9router:
curl -H "Authorization: Bearer $NINE_ROUTER_API_KEY" https://9router.com/v1/modelsGagal refresh
Common causes:
- Invalid API key.
- Base URL missing protocol.
- Network/TLS issue.
- 9router instance returned no models.
The extension redacts long token-like strings and your literal API key from errors.
Interactive setup not available
/9router setup needs Pi TUI context. In non-interactive/headless mode, use env vars or edit:
~/.pi/agent/9router-config.jsonEnv var beginning with digit fails
Use NINE_ROUTER_API_KEY instead of 9ROUTER_API_KEY.
Development
git clone https://github.com/ricatix/pi-9router.git
cd pi-9router
bun install
bun run typecheck
bun test
bun run buildProject structure
pi-9router/
├── src/
│ ├── index.ts # Pi extension entrypoint
│ ├── auth.ts # env auth and base URL normalization
│ ├── discovery.ts # 9router /models discovery
│ ├── enrichment.ts # models.dev metadata enrichment
│ ├── validation.ts # credential validation helper
│ ├── models.ts # Pi/OpenCode model mapping helpers
│ ├── state.ts # config/cache persistence
│ ├── commands.ts # /9router command router
│ ├── web.ts # reserved web helper module
│ ├── errors.ts # custom errors
│ ├── util.ts # hashing/redaction utilities
│ └── types.ts # local shared types
├── tests/
│ └── core.test.ts
├── package.json
├── tsconfig.json
└── README.mdValidation
Run before pushing or publishing:
bun run typecheck
bun test
bun run buildPublishing checklist
Before publishing:
- Verify package metadata in
package.json. - Run validation commands.
- Confirm
dist/is fresh. - Confirm README install commands point to the intended npm package.
- Confirm no secrets in config, cache, shell history, or test fixtures.
- Pick the next version (semver):
MAJOR.MINOR.PATCHfor stable,MAJOR.MINOR.PATCH-rc.Nfor pre-release. - Push the tag — the workflow handles the rest.
Stable release:
git tag v0.2.0
git push origin v0.2.0
# workflow runs typecheck + test + build + npm publish --provenancePre-release (lands under the next dist-tag, leaves latest untouched):
git tag v0.2.0-rc.1
git push origin v0.2.0-rc.1Manual one-off publish (requires npm login + 2FA OTP, no provenance):
bun install
bun run typecheck
bun test
bun run build
npm publish --otp=<code>Install published package:
pi install npm:pi-9router
# or pin a pre-release
pi install npm:pi-9router@nextRelease & CI/CD
The .github/workflows/publish.yml workflow runs on every v*.*.* tag push and:
- Installs Bun + Node 24 + latest
npm(npm 11.5.1+ is required for OIDC). - Runs
bun install --frozen-lockfile,bun run typecheck,bun test,bun run build. - Syncs
package.json#versionto the pushed tag. - Publishes to npm with
--provenance. Pre-release tags are published under thenextdist-tag.
Why Node 24 / npm 11.5.1+
Trusted publishing via OIDC requires npm 11.5.1+ to perform the token exchange with npmjs.com. Node 22 ships with npm 10.x, which silently fails the OIDC handshake and surfaces a misleading E404 'package@version' is not in this registry error. Node 24 LTS ships with npm 11.x and works out of the box.
Why no registry-url on setup-node
When you set registry-url on actions/setup-node@v4, the action writes a //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} line to .npmrc. With no NODE_AUTH_TOKEN secret configured (the intended state for OIDC), the placeholder expands to an empty string. npm then treats that as "auth is configured" and short-circuits the OIDC token exchange. The workflow sets the registry via npm config set instead.
One-time setup
After the first manual publish, configure the trusted publisher at:
https://www.npmjs.com/package/pi-9router/access
with:
| Field | Value |
| --- | --- |
| Repository owner / org | ricatix |
| Repository name | pi-9router |
| Workflow filename | publish.yml |
| Environment name | (leave empty) |
After that, every push of a v*.*.* tag is published automatically with no NPM_TOKEN secret.
Security
- API keys are sent only to the configured 9router base URL through
Authorization: Bearer .... - Status output shows only
setorunset, never key values. - Config/cache files are written under
~/.pi/agent/. - Config files are restricted to owner read/write where the platform allows it.
- Model cache contains model metadata, not credentials.
- Do not commit local
~/.pi/agent/9router-config.jsonor API keys.
Report security issues privately through GitHub once repository security advisories are enabled:
https://github.com/ricatix/pi-9router/security
Roadmap
- Stable web search/fetch tools after 9router web-route contract is finalized.
- Full reasoning/thinking mapping when Pi and 9router expose a stable compatible shape.
- More integration tests against a live Pi extension runtime.
