pi-multiprovider

Same-provider multi-account pooling, OAuth storage, and safe in-stream auth failover for Pi

Packages

Package details

extension

Install pi-multiprovider from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:pi-multiprovider
Package
pi-multiprovider
Version
0.9.1
Published
Sep 19, 2026
Downloads
3,837/mo · 747/wk
Author
monotykamary
License
MIT
Types
extension
Size
340.1 KB
Dependencies
0 dependencies · 3 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions/multiprovider.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

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.

npm version checks pi extension Node 22 license


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.
🔃 /switch-account Session pin to one pooled account for the current model—restored when the session is resumed; pool settings untouched.
🧬 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.
🧱 Error tolerance before switching Absorbs up to 3 pre-output errors on the same account before failing over, so one blip never pays a cold-cache switch.
🪪 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-multiprovider

The 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-multiprovider

From the new GitHub repository:

pi install git:github.com/monotykamary/pi-multiprovider

From a local checkout:

bun install
bun run build
pi install /absolute/path/to/pi-multiprovider

For one development run:

pi -e /absolute/path/to/provider-extension \
   -e /absolute/path/to/pi-multiprovider/extensions/multiprovider.ts

Quick start

Start Pi after installing both extensions, then run:

/multilogin

The flow:

  1. Searches providers and authentication methods exactly where Pi's /login UI does.
  2. Opens the pool manager, a settings view mirroring Pi's /settings: fuzzy search, inline value cycling, and drill-in submenus.
  3. 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.
  4. 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.
/vprovider [id] Create and edit virtual providers that map one model across multiple provider models.
/accounts Inspect pool policy, account status, in-flight leases, failures, and cooldowns.
/switch-account [label] Pin this session to one pooled account of the current model's provider, or return to automatic selection. The choice is restored the next time the session is resumed.

Pool strategies

Strategy Selection behavior Good for
Round robin Starts at the first healthy account in pool order (the main account) and spills over to later accounts only while earlier ones are unavailable. Unbiased pools rotate through healthy accounts in pool order from a random starting account, and differing weights shape traffic shares. A primary subscription with backup 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.

First-account bias keeps every new session on the account listed first in the pool—Pi default (upstream) when included, otherwise the first stored account—so you stop seeing sessions start on a backup account while the main one has plenty of usage. Integrations that want even request rotation register with selectionBias: 'none', which restores the classic rotate-through-healthy-accounts behavior: accounts rotate in pool order (the order they are configured, never re-sorted by id), the rotation starts at a random account so restarts do not favor the same one, and differing per-account weights share traffic smoothly instead of being ignored.

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. /switch-account sets the pinned account explicitly for one session without touching these settings. Pi Fabric participant agents inherit that pin through PI_MULTIPROVIDER_SESSION_PINS and rebind it to the child session, so spawned workers keep the operator's chosen account.

Error tolerance and failover compaction

A rejected account is not abandoned on the first error. Each stream absorbs up to errorsBeforeSwitch (default 3, configurable in the /multilogin Scheduler panel) pre-output errors on the same account—separated by a short pause—before releasing the lease, applying the failure cooldown, and moving to the next account. Errors after output has started and non-retryable failures surface immediately, exactly as before.

When pi-fabric is installed, failing over to a different account first compacts the session with fabric's deterministic, LLM-free compaction engine. The failing request surfaces its error, the session compacts while the retry backoff runs, and the retry lands on the next account with a small context instead of a huge cold prefill. This is the default behavior; without fabric installed, streams rotate accounts inline as before.

Virtual providers

A virtual provider maps one model to multiple provider models. Sessions are spread across the backing providers with unbiased round robin—no first-provider favoritism—while session affinity pins each session to one backend, so prompt caches stay warm between requests and every subscription sees roughly its share of sessions.

Create one with /vprovider:

  1. Choose Create new virtual provider, then set the provider id and virtual model id.
  2. Add one or more backing provider models—pick any registered provider and one of its models from a fixed-height, type-to-filter list. Toggle, reweight, or remove backends at any time.
  3. Save and apply. The virtual model appears in /model under the virtual provider's id.

Behavior details:

  • Each request resolves auth at the backing provider layer: the provider's own ambient credential (Pi /login, auth.json, environment) or, when the backing provider has a multiprovider pool, its pooled accounts with their own failover.
  • A failing backend fails over to the next one before any output streams; the failed backend cools down under the same scheduler policies as account pools.
  • /switch-account works on virtual models too: pin the session to one backing provider model, or return to automatic rotation.
  • Virtual provider configs are stored (credential-free) in multiprovider-auth.json next to the account pools.
  • Mixing backends from different model families is allowed, but the virtual model advertises the first healthy backend's context window and pricing, and prompt caches never transfer between providers.
  • Virtual models capture each backing model's metadata (reasoning support, thinking-level map, context window, pricing) when you pick it, so /thinking and per-model thinking memory (pi-model-sort) work across restarts and session resume — even before backing providers register. Stored configs are healed automatically on the next session start.

Switching accounts for one session

/switch-account lists every account pooled under the current model's provider—including Pi default (upstream) while its credential is configured—and pins the choice to the current Pi session:

  • The pin is session-scoped. Pool strategy, affinity, weights, and priorities stay untouched. Every switch is recorded in the session file as a custom entry that is never sent to the model, so resuming the session restores the last switched account instead of falling back to the pool strategy.
  • New requests from this session use the pinned account, even while the pool's session affinity is off.
  • If the pinned account cools down, another account serves temporarily and the session returns to it once it recovers. Removing or disabling the account drops the pin for the rest of the session.
  • Automatic—or /switch-account auto—clears the pin so the pool strategy selects again; the cleared state is recorded too, so a resumed session stays automatic.
  • /switch-account work switches directly when the label matches exactly or by unique prefix.
  • If the pinned account was removed or disabled before the session is resumed, the session warns once and falls back to automatic selection.

In-flight requests keep their leased account; only new requests observe the switch. Sibling extensions can follow switches—and the account a resumed session restores—through the pi-multiprovider:service event.

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 credentials

PI_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 /accounts snapshots 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:

  1. The provider failed before text, thinking, or tool-call output became visible.
  2. The failure is account-local or transient.
  3. 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, and 5xx transient 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 31 deterministic tests covering scheduling, session account pinning, the service announcement, 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.

Session account service event

The bundled extension announces a small in-process service on pi-multiprovider:service (emitted at load and on session start) so sibling extensions can follow the session's active pooled account—for example, to refresh account-scoped subscription usage views after /switch-account or a resume:

import {
  MULTIPROVIDER_SERVICE_EVENT,
  type MultiProviderServiceAnnouncement,
} from "pi-multiprovider"

pi.events.on(MULTIPROVIDER_SERVICE_EVENT, value => {
  // Duck-check value.getActiveAccount / resolveActiveAccountAuth /
  // onActiveAccountChanged, or cast to MultiProviderServiceAnnouncement.
})
  • getActiveAccount(providerId, ctx) — the session's effective account: the explicit /switch-account pin, else the scheduler's last selection while pool affinity is on. undefined means selection is automatic or upstream, and callers should fall back to their own credential resolution.
  • resolveActiveAccountAuth(providerId, ctx, signal?) — resolves (refreshing OAuth under the account-store lock when needed) the active stored account's credential as { accessToken, label, source? }. Returns undefined for the upstream account or when nothing is active, so consumers keep their existing fallback chain.
  • onActiveAccountChanged(providerId, callback) — fires after /switch-account pins or clears, and when a session start replays a recorded pin (resume, fork, or session switch). The event carries the triggering ctx and the new active account—undefined when the replayed decision returned the session to automatic selection—so account-scoped widgets repaint with the restored account instead of waiting for their next poll. Listeners attached after a replay can rely on their own session start, which observes the already-restored pin.

Credential values are never broadcast in the event payload itself; only extensions that invoke the resolver receive them, and the private multiprovider-auth.json store is never read directly by consumers.

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 implicit session affinity reset when Pi reloads or replaces the extension runtime; credentials, pool settings, and the /switch-account decisions recorded inside a session persist with it.

Current limits:

  • Deferred fetch/cancel operations are not lifted yet; stream and streamSimple are the supported failover paths.
  • A broken or revoked OAuth credential in Pi's primary auth.json can fail during Pi's pre-stream refresh before account selection. Run /logout for 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.
  • Provider-owned integrations with a custom affinityKey are invoked with a minimal context by /switch-account; keys that depend on request message history cannot be reproduced there and fall back to the Pi session id.

Development

bun install
bun run typecheck
bun run test
bun run build

The full release gate is:

bun run check
npm pack --dry-run

See 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-multiprovider during local development.
  • Built on Pi's native Provider, auth interaction, and TUI component APIs.

License

MIT