@jiajun0413/pi-provider-manager
Roundrobin failover engine for the pi coding agent — virtual provider pooling multiple candidates with sticky failover and cooldown
Package details
Install @jiajun0413/pi-provider-manager from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@jiajun0413/pi-provider-manager- Package
@jiajun0413/pi-provider-manager- Version
0.6.1- Published
- Aug 3, 2026
- Downloads
- 492/mo · 492/wk
- Author
- jiajun0413
- License
- MIT
- Types
- extension
- Size
- 38.4 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@jiajun0413/pi-provider-manager
Roundrobin failover engine for the pi coding agent.
Registers a virtual roundrobin provider that pools multiple real model candidates behind one virtual model, with sticky failover, idle-timeout guards, and per-candidate cooldown.
Attribution
This package is a fork and rebuild of @arcaneorion/pi-provider-manager@0.3.9 (upstream npm package; no public Git repo). The roundrobin failover engine architecture is the original author's work, used here under MIT.
This fork fixes a mid-stream idle-timeout bug (a candidate stream that starts then goes silent no longer hangs forever), hardens failover against unstable relays (upstream aborted is now a cooldown-able failure, not a turn-killing cancel; auth lookup is bounded by the idle guard), and strips the visual config panel and on-disk health store, keeping only the failover engine.
Install
pi install npm:@jiajun0413/pi-provider-manager
Restart pi. (pi loads .ts extensions via jiti — no build step.)
Features
- Roundrobin failover — one virtual
roundrobinprovider pools multiple provider/model candidates. Sticky strategy with automatic failover on first-response timeout or stream error. Each preset becomes a selectable virtual model. - Mid-stream idle timeout — a candidate stream that starts then goes silent errors out after
timeoutMsinstead of hanging forever (prevents ghost subagent sessions). - No mid-stream replay — once content or tool calls have been emitted, an error is terminal rather than replayed on another provider, to avoid duplicating output.
- Upstream-aborted is a failure, not a cancel — a relay that drops the socket / cancels upstream and surfaces
stopReason:"aborted"is cooled down and the pool rotates, instead of killing the whole turn. Only a real user abort (the request's ownAbortSignal) short-circuits. - Auth-bounded idle guard — API-key/header resolution races against the same
timeoutMs+ attempt abort as stream chunks, so a hung key lookup can't stall the turn. - Entry-time model population — virtual models register at load time with placeholder candidates so they appear in
/modeland static lists (pi-web's/api/models) before a session starts;session_startrebuilds with the real registry. - virtualModel inheritance — the
virtualModelblock is optional; omit it and metadata is inherited from the group's first candidate (see Inheritance).
Usage
Create
~/.pi/agent/roundrobin/config.json(the"default"group):{ "virtualModel": { "name": "GLM-5.2", "reasoning": true, "input": ["text"], "contextWindow": 1000000, "maxTokens": 128000, "thinkingLevelMap": { "max": "max" }, "compat": { "thinkingFormat": "deepseek" } }, "candidates": [ { "provider": "k", "model": "glm-5.2" }, { "provider": "wg", "model": "glm-5.2" } ], "timeoutMs": 16000, "cooldownMs": 120000, "strategy": "sticky", "log": true }(Optional) Add more groups as
*.jsonfiles underpresets/. Each file's stem (name without.json) becomes aroundrobin/<name>model.In pi, use
/modelto selectroundrobin/default(or any preset name).Requests now flow through the failover engine. If the current candidate times out or errors before the stream starts, the next candidate is tried automatically. If every candidate fails, the engine waits for the earliest cooldown and retries from the preferred candidate — bounded to 3 passes before the turn ends with the last error.
A mid-stream error is returned immediately rather than replayed (content or tool calls may already have been emitted).
Sticky strategy — stays on the current candidate until it fails. After cooldown, returns to the preferred (first) candidate automatically.
Multi-preset routing
Each preset under ~/.pi/agent/roundrobin/ registers as an independent virtual model whose model id equals the preset file stem (the .json suffix is stripped). config.json is the reserved "default" group; every *.json file under presets/ is an additional group. In /model all virtual models appear at once — pick roundrobin/<name> to route to that group's pool. Health and currentIndex are isolated per preset.
Configuration
~/.pi/agent/roundrobin/config.json
| Field | Default | Meaning |
|---|---|---|
virtualModel |
(optional) | Virtual model metadata: name, reasoning, input, contextWindow, maxTokens, thinkingLevelMap, compat. id is forced to the group name. Omitted fields are inherited from the first candidate (see Inheritance); omit the whole block for homogeneous groups. |
candidates |
[] |
{ provider, model }[] referencing entries in models.json. Unknown pairs are skipped (if all are invalid the group is enabled=false). |
timeoutMs |
30000 |
Idle timeout per stream event — first byte and every mid-stream chunk race against this. |
cooldownMs |
60000 |
How long a failed candidate is skipped before it can be retried. |
strategy |
sticky |
sticky / round-robin / primary. |
log |
true |
Append failover events to ~/.pi/agent/roundrobin/roundrobin.log. |
Presets
*.json files under ~/.pi/agent/roundrobin/presets/<name>.json — each is a standalone config (same shape as config.json) and becomes a roundrobin/<name> virtual model (id = <name>, the .json suffix stripped). bak.* files are skipped. A corrupt preset is skipped silently; it never breaks the engine.
Inheritance
The virtualModel block is optional. When you omit a field (or the whole block), the engine fills it from the group's first resolved candidate — the Model from modelRegistry.find(provider, model) in your models.json. Precedence is three-tier:
- Explicit
virtualModel.<field>in the preset wins. - Else inherit from the first resolved candidate's
Model. - Else a built-in default.
A homogeneous group (multiple providers of the same model) needs no virtualModel at all:
{
"candidates": [
{ "provider": "k", "model": "glm-5.2" },
{ "provider": "wg", "model": "glm-5.2" }
],
"timeoutMs": 16000,
"cooldownMs": 120000,
"strategy": "sticky"
}
For heterogeneous groups (mixed models, e.g. a "power" pool of Grok + Claude + Deepseek), keep an explicit virtualModel to unify behavior across the pool (force a single compat.thinkingFormat, or a conservative contextWindow).
Inheritance happens at
session_start, when the real model registry is available. Pre-session_startthe engine registers with placeholder candidates so virtual models still show in/modeland static lists; fields are then filled from real candidates once a session starts.
Architecture
index.ts ← entry: registers the roundrobin provider
extensions/
├── model-roundrobin.ts ← failover engine (streamRoundRobin → tryCandidates)
└── rr-config.ts ← read config.json + presets/ → GroupConfig[]
Cooldown (failure tracking) is process-local in-memory — a failed candidate's cooldown resets when pi restarts. This is by design: cooldown is an in-process failover notion, not a durable statistic.
License
MIT