pi-multiprovider
v0.2.7
Published
Same-provider multi-account pooling, OAuth storage, and safe in-stream auth failover for Pi
Maintainers
Readme
🔀 pi-multiprovider
Multi-account credential pooling and safe same-provider failover for Pi
One provider ID. One model ID. As many API-key or OAuth accounts as you need.
Pi normally owns one stored credential per provider. pi-multiprovider adds a second, provider-scoped credential store and lifts the provider's native stream in place. Each request leases an account, resolves that account's auth, and delegates to the original provider without inventing aliases such as zro-2 or changing the selected model.
If an account fails before visible output, the lift can cool it down and retry another account inside the same logical stream. Once text, thinking, or a tool call is visible, replay stops—duplicate output is worse than a surfaced error.
Why multiprovider?
| | Capability | What it does |
| :-: | --- | --- |
| 🔐 | Multiple credentials | Store API keys and provider-native OAuth credentials per provider. |
| 🪄 | /multilogin | Reuses Pi's searchable provider selector and login dialog, then opens a searchable settings-style pool manager for drilling into every row inline. |
| 🔀 | Four pool strategies | Round robin, weighted round robin, least in flight, or priority failover. |
| 🧬 | Upstream merge | Optionally treats Pi's normal /login, auth.json, environment, or ambient credential as another account—editable inline like any stored account. |
| 🩺 | Health-aware leases | Tracks in-flight work, failures, cooldowns, session affinity, and retry exclusions. |
| 🛡️ | Stream-safe failover | Suppresses a rejected attempt's start/error events and retries only before user-visible output. |
| 🪪 | Stable identity | Provider ID, model ID, model picker entries, routing, and session history remain unchanged. |
Install
Requires Node.js 22.19+ and Pi 0.84.3+.
pi install npm:pi-multiproviderThe npm package registers the extension automatically. Install the provider extension you want to pool as usual; for example:
pi install npm:pi-zro-provider
pi install npm:pi-multiproviderFrom the new GitHub repository:
pi install git:github.com/monotykamary/pi-multiproviderFrom a local checkout:
pnpm install
pnpm build
pi install /absolute/path/to/pi-multiproviderFor one development run:
pi -e /absolute/path/to/provider-extension \
-e /absolute/path/to/pi-multiprovider/extensions/multiprovider.tsQuick start
Start Pi after installing both extensions, then run:
/multiloginThe flow:
- Searches providers and authentication methods exactly where Pi's
/loginUI does. - Opens the pool manager, a settings view mirroring Pi's
/settings: fuzzy search, inline value cycling, and drill-in submenus. - The Add account row asks for a non-secret label and runs the provider's own login implementation—including pasting an API key for providers without an interactive flow—then returns to the manager.
- Every other row edits live settings: pool strategy and session affinity, an Accounts section grouping every pooled credential—Pi default (upstream) plus stored accounts—with per-account weight (traffic share) and priority (failover order), and scheduler cooldowns.
Add as many accounts as you need from the same manager. Remove credentials from an account's submenu or with /multilogout; Pi's regular /logout and auth.json remain independent.
Commands
| Command | Purpose |
| --- | --- |
| /multilogin [provider] | Open the pool manager: strategy, affinity, upstream, account, and scheduler settings, plus adding or removing accounts. |
| /multilogout [provider] | Remove an account saved by /multilogin. |
| /accounts | Inspect pool policy, account status, in-flight leases, failures, and cooldowns. |
Pool strategies
| Strategy | Selection behavior | Good for | | --- | --- | --- | | Round robin | Rotates through healthy account IDs. | Even distribution across similar accounts. | | Weighted round robin | Uses smooth weighted scheduling. | Accounts with different quotas or spend limits. | | Least in flight | Selects the healthy account with the least active work. | Concurrent agents and uneven request duration. | | Priority failover | Uses the lowest-priority number until it becomes unhealthy. | Primary/backup credentials. |
Session affinity can pin a healthy account to the current Pi session. Explicit retry exclusions always win, so a rejected account is not selected twice for the same logical request. Switch strategies, affinity, and per-account weight and priority at any time inside /multilogin.
How auth merging works
Pi still owns its one normal provider credential. Multiprovider owns additional credentials:
~/.pi/agent/auth.json Pi /login and normal credential
~/.pi/agent/multiprovider-auth.json extra pooled credentialsPI_CODING_AGENT_DIR relocates both files in the usual way. The multiprovider file is:
- created with mode
0600 - written through same-directory atomic renames
- protected by a cross-process lock with stale-lock recovery
- versioned for future migrations
- never included in
/accountssnapshots or logs
API-key credentials use the provider's native resolve() method, including provider-scoped environment values. OAuth credentials use the provider's native login(), refresh(), and toAuth() methods; refresh runs under the account-store lock with Pi's five-minute validity window.
When Pi default is enabled, the lifted auth method first lets Pi resolve its normal credential. Multiprovider marks only the names—not values—of credential-specific headers and environment fields. If a stored account is selected, stale upstream auth fields and credential-specific base URLs are removed before transport.
Inside /multilogin the Pi default credential appears in the pool's account list like any stored account: relabel it, raise or lower its weight (default 1) and priority (default 0), or disable it so only multilogin accounts run. When no pool exists yet but /login already has a credential configured, it is listed as pending so it can be preconfigured before the first stored account. The credential value itself stays Pi-owned—rotate or replace it through /login. Pool, account, upstream, and scheduler settings persist alongside the credentials in multiprovider-auth.json. The manager's Scheduler section overrides the global failure cooldowns live: rate limit (60s), quota (15m), auth (5m), transient base (1s, doubling per consecutive failure), and the 60m cap.
Failover semantics
An account can be retried when all of these are true:
- The provider failed before text, thinking, or tool-call output became visible.
- The failure is account-local or transient.
- Another enabled account is healthy and has not been attempted.
Default retry classes include:
- HTTP
401/403, invalid keys, tokens, grants, or expired credentials - HTTP
402, quota exhaustion, or out-of-credit messages - HTTP
429, rate limits, overload, or too-many-requests responses - HTTP
408,425, and5xxtransient failures
The lift sets provider-local retries to zero by default so there is one retry owner. Provider integrations can override classification and cooldown duration.
Real ZRO proof
The implementation was exercised against the actual sibling pi-zro-provider and two independently stored LocalTerm credentials. Secret values were passed only through process environment into isolated mode-0600 test stores and were never printed.
| Probe | Result |
| --- | --- |
| First stored ZRO API key, no ZRO_API_KEY environment fallback | ZRO_FIRST_OK |
| Second stored ZRO API key, no ZRO_API_KEY environment fallback | ZRO_SECOND_OK |
| Priority-1 synthetic invalid key → priority-2 valid key, same zro/deepseek-v4-flash-0731 stream | ZRO_FAILOVER_OK |
The package also has direct Pi runtime probes and 21 deterministic tests covering scheduling, stream integrity, cancellation, secure storage, concurrent mutation, OAuth refresh locking, upstream auth scrubbing, upstream preference persistence, scheduler settings, pool-only availability, and simulated API-key/OAuth login flows.
Provider integration API
The built-in managed store works with native providers and legacy pi.registerProvider() configurations composed by Pi. Providers with an existing account inventory can register their own opaque references instead:
import { registerMultiProvider } from "pi-multiprovider";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function providerExtension(pi: ExtensionAPI) {
registerMultiProvider(pi, {
id: "example",
label: "Example",
accounts: async () => [
{
id: "work",
label: "Work",
authKind: "api-key",
credentialRef: "opaque:work",
weight: 2,
priority: 1,
},
{
id: "backup",
label: "Backup",
authKind: "oauth",
credentialRef: "opaque:backup",
priority: 2,
},
],
async resolveAuth(account, signal) {
return resolveProviderOwnedCredential(account.credentialRef, signal);
},
});
}Credential references are intentionally opaque. Account inventory, refresh, billing, quota, and provider-specific metadata remain provider-owned. Re-announce after a provider re-registers dynamically; the bundled extension also reconciles its lift before every agent run.
For direct composition, the public package exports MultiProviderService, liftProvider, MultiAuthStore, createManagedIntegration, mergeProviderAuth, and all scheduler/integration types.
Safety boundaries
- No replay after output. A failure after any content event is surfaced unchanged.
- No secret snapshots. Public account state contains labels and health only, never credential references or credential values.
- Case-insensitive header replacement. Selected auth replaces matching headers and can remove obsolete auth fields.
- Lease lifetime equals stream lifetime. Success, failure, and cancellation release capacity exactly once.
- Provider re-registration is expected. The extension re-lifts current provider objects before agent execution, covering dynamic model refreshes used by provider packages.
- Health is in memory. Cooldowns and affinity reset when Pi reloads or replaces the extension runtime; credentials and pool settings persist.
Current limits:
- Deferred fetch/cancel operations are not lifted yet;
streamandstreamSimpleare the supported failover paths. - A broken or revoked OAuth credential in Pi's primary
auth.jsoncan fail during Pi's pre-stream refresh before account selection. Run/logoutfor that provider or repair the primary login; extra pooled OAuth refreshes are independently isolated. - If both a stored pool and a provider-owned integration register for one ID, the stored pool wins and Pi displays a warning.
Development
pnpm install
pnpm run typecheck
pnpm run test
pnpm run buildThe full release gate is:
pnpm run check
npm pack --dry-runSee SECURITY.md for the local credential threat model and private vulnerability reporting.
Acknowledgments
- Inspired by hjanuschka/pi-multi-pass, while keeping one provider identity and moving retries down to the stream boundary.
- Scheduler and credential-ownership semantics mirror the lift used by
dsh-multiproviderduring local development. - Built on Pi's native
Provider, auth interaction, and TUI component APIs.
License
MIT
