pi-provider-chain

Pi 扩展:虚拟模型 + 链路(chain)路由 + 额度耗尽自动降级(quota / subscription / overdue 类错误自动切到后备链路)

Packages

Package details

extension

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

$ pi install npm:pi-provider-chain
Package
pi-provider-chain
Version
0.2.1
Published
Oct 16, 2026
Downloads
741/mo · 66/wk
Author
yansongda
License
MIT
Types
extension
Size
103.6 KB
Dependencies
0 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./extensions/provider-chain.ts"
  ]
}

Security note

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

README

English | 简体中文

pi-provider-chain

An extension for Pi: virtual models + automatic fallback when quota runs out.

It registers a virtual model for every configured entry (mounted under the yansongda provider by default, e.g. yansongda/deepseek-v4.1-flash) and routes requests to chain[0]; as soon as a quota / subscription / overdue-class error appears, it advances along chain[1..] and writes the degraded state into a shared state file, so it takes effect across sessions, across processes, and across parent/child subagents.

Features

  • Only quota-class errors trigger fallback: transient 429 rate limits, 5xx, and network errors are left alone and handed back to Pi's own retry.
  • Two trigger paths: the retry fast path (retryable errors); the agent_before_settle backstop (non-retryable errors — the failed message is dropped and the run continues once).
  • Cache-friendly: retry stays on the same level, continuation reuses the previous physical model, so the prompt cache / thinking signature is invalidated as rarely as possible.
  • Explainable: a notify message, plus a footer that permanently shows the current target physical model (→ while the chain's first provider is in use; ↓ plus the earliest recovery time and the provider it belongs to once degraded), plus a visible transcript entry (prefix [chain], never sent to the model context).
  • Automatic recovery: the degradation window is parsed from the reset at ... in the error, falling back to a default duration; once the window expires, the first successful primary request reports recovery.
  • Zero runtime dependencies: only the host packages Pi provides (as peers), no transitive dependencies.

Installation

# Install from npm
pi install npm:pi-provider-chain

# Local path (for development; changes need no reinstall)
pi install /Users/yansongda/000-Coding/pi-provider-chain
pi list

After installing, configure ~/.pi/agent/provider-chain.json (or $PI_CODING_AGENT_DIR/provider-chain.json); a complete example lives in examples/provider-chain.json and field descriptions in examples/README.md. Run /reload or start a new session for changes to take effect. With pi-subagents, the child session also has to load this extension (one npm:pi-provider-chain line is enough) — see Use with pi-subagents.

Configuration

The minimal working config (you only have to declare one primary model; see the example file for every optional field):

{
  "virtualProvider": "yansongda",
  "models": [
    {
      "id": "deepseek-v4.1-flash",
      "name": "DeepSeek V4.1 Flash · auto",
      "chain": ["volcengine/deepseek-v4.1-flash", "workbuddy/deepseek-v4.1-flash"]
    }
  ]
}
Field Required Default Description
virtualProvider ✅ - Provider id the virtual models are mounted under (you may reuse an existing provider, or use a brand-new id)
models[].id ✅ - Virtual model id; must not be identical to a physical model id under virtualProvider; duplicates within one config raise an error
models[].name - models[].id Name shown in the /model list (ignored when not a string)
models[].thinkingLevels - all levels Thinking levels available for selection (non-array, or empty after filtering, falls back to all levels)
models[].input - host default (text + image) Input kinds this virtual model accepts; only text / image are valid (other values are dropped, duplicates removed); when omitted the field is not sent
models[].contextWindow / maxTokens - not declared Limits shown before the first response (ignored when not a positive number)
models[].chain[] ✅ - The chain: chain[0] is the primary, everything after it is an ordered backup (provider/id); an invalid chain[0] raises an error, later invalid entries are silently ignored; a total chain length ≤ 5 is recommended (the common gateway cap — an over-long chain makes one failure pay the failure cost on provider after provider)
degradePatterns[] - 14 built-ins Keywords that trigger a fallback on match (case-insensitive; a non-empty list fully overrides the defaults, an empty/all-whitespace list falls back to the built-in defaults)
resetPattern - built-in regex Regex that parses the recovery time out of the error text (capture group 1/2 is date/time, optional group 3 is the timezone)
defaultDegradeMs - 3600000 (1h) Fallback duration when no recovery time can be parsed (clamped when it exceeds maxDegradeMs)
maxDegradeMs - 2592000000 (30d) Hard cap on the fallback duration; clamps the parsed reset time, defaultDegradeMs, and a manual /chain degrade <duration>
transcriptNotice - true Whether to append a visible entry to the transcript (does not affect the model context)
stateFile - ~/.pi/agent/provider-chain-state.json State file path (supports ~)

Built-in default degradePatterns: AccountQuotaExceeded, QuotaExceeded, AgentPlanQuotaExceeded, SetLimitExceeded, InvalidSubscription, subscription has expired, AccountOverdueError, ServiceOverdue, overdue balance, usage quota, exceeded the quota, insufficient_quota, out of budget, billing.

Plain words/phrases automatically get word boundaries (\bbilling\b) to reduce false positives; entries containing _, digits, or camelCase are matched verbatim.

Use with pi-subagents

Virtual models are registered by this extension when it loads. A foreground child inherits the providers the parent's extensions registered, but not virtual models registered with pi.registerVirtualModel(), so a subagent that inherits a virtual model (for example the default model yansongda/s-main) fails to start because the model does not resolve; the error states It inherits the providers they registered, but not virtual models registered with pi.registerVirtualModel().. Let the child session load this extension as well:

{
  "subagents": {
    "defaultSubagentOnlyExtensions": ["npm:pi-provider-chain"]
  }
}

Put this in ~/.pi/agent/settings.json (or $PI_CODING_AGENT_DIR/settings.json). Values go through Pi's package-source resolution: npm:pi-provider-chain is installed from npm into <agentDir>/tmp/extensions/npm/<hash>/ on the first child launch and reused — no path to remember; an extension file path (absolute, ~/ expanded) works too.

  • Use a path for local development: the npm: form loads the published copy from npm, which is not the same tree as a pi install <local path> working copy and leads to parent/child version drift; then use the absolute path to extensions/provider-chain.ts in the repository.
  • The npm: form needs network access for that first install; PI_OFFLINE=1 silently skips the source (the child gets no extension), so use a path in that case.
  • subagents.defaultSubagentOnlyExtensions only adds loads to child sessions and does not change ambient extension discovery; do not use subagents.defaultExtensions instead, since its mere presence (even as []) disables ambient discovery for children.
  • To affect a single agent, put subagentOnlyExtensions: (additive, keeps ambient) or extensions: (present at all — including [] — disables ambient discovery for that child) in its frontmatter.
  • A background (async: true) child loads ambient extensions by default and usually needs no extra configuration.
  • A capability ceiling with capabilityCeiling.denyExtensions overrides everything above.
  • Verify by launching a subagent that inherits a virtual model, or by having the agent call subagent({ action: "get", agent: "<name>" }) to see the effective Subagent-only extensions; /subagents-doctor shows diagnostics.
  • Children share provider-chain-state.json with the parent session, so the degradation level applies across processes.

Commands

Command Description
/chain or /chain status Currently selected model, config and state file paths, and every virtual model's chain and degradation state (subcommands are completed)
/chain degrade [duration] Manually fall back to the first backup (30m / 2h / 1d; defaults to defaultDegradeMs, capped by maxDegradeMs)
/chain reset Clear the degradation state of the provider the current model belongs to (aliases primary / clear)
/chain reload Reload the extension and config (equivalent to /reload)

State file

~/.pi/agent/provider-chain-state.json by default:

{
  "version": 1,
  "degraded": {
    "volcengine": { "level": 1, "until": 1793203199000, "reason": "AccountQuotaExceeded", "at": 1792055998922 }
  }
}
  • The key is the provider of chain[0] (all virtual models under one provider share the level).
  • Writes use tmp + rename atomic replacement; concurrent writes are merged monotonically ("for the same key keep the larger level"), so a parent and a child process never push the level back on each other.
  • There is no file lock between read and write: if two processes degrade different providers at the same moment, a record can theoretically be lost; the provider's next request will degrade again, so it self-heals.
  • Every write prunes historical entries older than until + 24h.
  • After recovery a tombstone entry with level: 0, reason: "recovered" is written: it makes the "recovered" notice appear exactly once, and under concurrency it never overwrites an active degradation another process just wrote (the monotonic merge keeps the deeper level).
  • When the write fails, the fallback still applies in this process, and a message states that "the state file write failed, so this fallback only applies to the current process".

How it works

Selected yansongda/deepseek-v4.1-flash (virtual model)
        │  route(request, ctx)
        ▼
read state file → level (0 = primary, 1..n = backup chain index; an expired window falls back to 0 automatically)
        │
        ├─ retry, and the error text matches degradePatterns → advance the level and write state (fast path)
        ├─ retry / continuation, and it failed or the previous model is the current level → reuse it (cache-friendly)
        ├─ otherwise walk forward from the current level for a model with credentials, then fall back backwards
        ▼
return the physical model + thinking level (the assistant message records the physical model)

Non-retryable quota errors (such as a 403 subscription expiry) go through the agent_before_settle backstop: on a match it writes the degraded state first, then drops that failed message and continues the run once; if the entry holding the failed message cannot be located (so it cannot be dropped), the state is still written so the next request goes to a backup.

The footer permanently shows the target physical model of the currently selected model (→ volcengine/… while the chain's first provider is in use; ↓ workbuddy/… · 10-28 23:59 恢复 volcengine while degraded — the timestamp is the earliest recovery among the chain's active degrade entries and the trailing name is the provider it belongs to) — degradation info for other models is listed in full by /chain status. Transcript entries are prefixed with [chain] and come in three visible kinds: degraded (kind: "degrade", with the limited provider from, the reason, and the trigger path trigger; the same sentence as the notify message), recovered (kind: "recover"), and routing error (kind: "error", which falls back to the primary but leaves the exception in the transcript for troubleshooting).

Development

bun install       # install devDeps / peers
bun run test      # bun test: run every node:test case under tests/
bun run typecheck # tsc --noEmit
bun run pack      # bun pm pack --dry-run, inspect the published contents

# The same suite also runs straight from TS on Node ≥ 22.18 (no test framework); CI guards this promise on Node 24
bun run test:node # equivalent to node --test "tests/**/*.test.ts"
extensions/provider-chain.ts      Wiring layer (registers virtual models / events / commands / renderers + ctx→ports adapter)
src/constants.ts                  Machine constants + user-facing message templates (leaf module, zero deps)
src/model-ref.ts                  provider/id parsing
src/degrade.ts                    Degradation decision and time parsing (pure functions)
src/state.ts                      State file read/write (atomic write / monotonic merge / pruning)
src/config.ts                     Config loading and validation
src/route-policy.ts               Routing decision (pure functions)
src/router.ts                     Degradation state machine and decisions (ports injected, unit-testable)
src/transcript.ts                 Transcript entry contract and session traversal (pure functions)
src/command.ts                    /chain argument parsing (pure functions)
src/ui.ts                         User-facing message assembly (notify / footer / transcript entries / command output)
examples/                         Copy-pasteable config example + field descriptions
tests/                            node:test unit tests (incl. an assembly integration test with a stubbed pi)
docs/                             Architecture and design documents (docs/README.md is the index)

License

MIT