npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@timmyjl12/pi-fallback-chains

v0.3.0

Published

Glob-keyed model fallback chains for pi. Per-model failover that walks a real ordered chain across providers, instead of one flat per-provider list.

Readme

@timmyjl12/pi-fallback-chains

Glob-keyed model fallback chains for pi.

When a provider fails, most fallback tools can only say "try the next provider". This one lets you say which model falls back to which models, in order, across providers — the shape oh-my-pi's retry.fallbackChains uses.

{
  "chains": {
    "cline-pass/cline-pass/glm-5.3-flash": [
      "cline-pass-gmail/cline-pass/glm-5.3-flash",
      { "model": "neuralwatt/glm-5.3-flash-flex", "thinking_level": "max" },
      "opencode-go/glm-5.3-flash",
      "openrouterTim/openrouter/fusion:xxs"
    ]
  }
}

Why not the alternatives

| Capability | pi core retry.* | pi-provider-fallback | this | |---|---|---|---| | Retries a failed request | yes | yes | yes | | Switches model/provider | no | yes | yes | | Per-model chains | no | no (per-provider) | yes | | Glob-keyed chain keys | no | no | yes | | Ordered cross-provider chain | no | no (sorted by availability) | yes | | Per-target thinking level | no | no | yes | | Configurable error patterns | no | no (hardcoded regexes) | yes | | Per-request failover budget | n/a | no (per-session pointer) | yes | | Reports inert targets | n/a | partial | yes |

pi core's models.json also has a per-model allowedFallbackModels, but it caps at three entries and is server-side — the provider has to honour it. This extension is client-side, unlimited in length, and works with any provider.

Install

pi install npm:@timmyjl12/pi-fallback-chains   # install
pi -e npm:@timmyjl12/pi-fallback-chains        # try for one run without installing

Configure

Config is read from the first path that exists, in this order:

  1. $PI_FALLBACK_CHAINS_CONFIG — used outright. If set and missing, that is reported as a misconfiguration rather than silently falling through to the paths below.
  2. <project>/.pi/pi-fallback-chains.json
  3. ~/.pi/agent/pi-fallback-chains.json

A project file therefore overrides your global one, and a global file is found even when the project has none. /fallback-chains lists every path that was probed.

Configuration is optional: with no file, recoverable provider failures get one final attempt on openrouterTim/openrouter/fusion:xxs. Set "terminalFallback": false to disable that tier, or "enabled": false to disable all recovery. An explicitly missing override, unreadable/malformed file, or explicitly unusable chain stays a configuration error; it does not silently enable Fusion.

Start from pi-fallback-chains.example.json.

Chain keys are globs over provider/modelId

Patterns are anchored at both ends and matched against the full provider/modelId string. * matches any run of characters including /, which is required because model ids are themselves namespaced:

cline-pass/cline-pass/glm-5.3-flash
└─ provider ┘└─── model id ───────┘

That gives the property you want — overlapping provider names stay separate:

| Pattern | cline-pass/cline-pass/glm-5.3-flash | cline-pass-gmail/cline-pass/glm-5.3-flash | |---|---|---| | cline-pass/* | ✅ | ❌ | | cline-pass-gmail/* | ❌ | ✅ | | * | ✅ | ✅ |

Also supported: ? for exactly one character. Everything else is literal (a . is a dot, not a wildcard).

Which chain wins

  1. An exact key with no wildcards.
  2. Otherwise the matching key with the most literal characters.
  3. Otherwise defaultChain, if set.
  4. Otherwise the terminal Fusion tier alone is available.

Configured non-Fusion candidates always precede Fusion. The original remaining candidates are retained when a replacement fails; that replacement's own configured chain is appended behind them (with deduplication), preserving cascading routes without losing the original queue. Fusion is removed from ordinary targets, even glob matches, and offered exactly once at the end. Its attempt is reserved beyond the ordinary budget. A failure on Fusion is terminal, including when Fusion was the initial model. Unavailable, out-of-scope, or quota-capped Fusion is reported, never forced.

So you can have a broad cline-pass/* chain plus a sharper cline-pass/cline-pass/kimi-k3 chain, and only kimi-k3 takes the sharper path.

Targets, and thinking effort

A target is either a glob or an exact model key. Thinking effort can be expressed three ways — all equivalent:

{ "chains": {
  "a/*": [ "neuralwatt/kimi-k3-flex:max" ],

  "b/*": [ { "model": "neuralwatt/kimi-k3-flex", "thinking_level": "max" } ],

  "c/*": { "thinking_level": "max", "targets": ["neuralwatt/kimi-k3-flex"] }
} }
  • :level suffix — :off|minimal|low|medium|high|xhigh|max.
  • thinking_level field, per target. thinkingLevel and level are accepted aliases.
  • Chain-wide thinking_level, applied to targets that don't set one. An explicit per-target level always wins.

Only recognised levels are stripped as suffixes, so real model ids survive: openrouter/fusion:xxs and z-ai/glm-5.2:free are matched as written.

Tunables

| Key | Default | Meaning | |---|---|---| | enabled | true | Master switch | | defaultChain | [] | Used when no key matches | | terminalFallback | true | One final openrouterTim/openrouter/fusion:xxs attempt, even without a config file; false excludes it from every route | | maxFailoversPerRequest | 4 | Ordinary swaps per user prompt, plus one reserved terminal attempt. Also read from omp's retry.maxRetries | | settleDelayMs | 0 | Extra grace after pi has fully settled, before rotating | | retryMode | "replay" | "replay" re-sends your prompt, "continue" sends retryInstruction | | retryInstruction | "Continue from where you left off." | Text used by "continue" | | compactBeforeRetry | true | Compact first if the target's context window is smaller | | requireNoToolCalls | false | Refuse to rotate when the failed run used tools (see below) | | notify | true | Show fallback notices | | restoreOnShutdown | true | Restore your session model on exit | | retryableStatusCodes | 404,408,409,425,429,5xx,52x | HTTP codes worth rotating for (404: the model is gone — another one serves) | | nonRetryableStatusCodes | 400,401,403,405,422 | Never rotate on these | | retryablePatterns | see example | Case-insensitive regexes | | nonRetryablePatterns | see example | Checked first, so auth beats a co-occurring 429 | | providerCapPatterns | see example | Errors meaning the whole provider is out of quota (see below) |

How a failure is handled

  1. An assistant turn ends with stopReason === "error"; the failure is captured on agent_end. Nothing happens yet, because pi may still auto-retry, auto-compact, or run a queued continuation after that event.
  2. The message is classified. nonRetryablePatterns wins over retryable ones; auth failures and context overflow never rotate (rotating cannot fix either).
  3. Pi reaches agent_settled — the event that means no automatic retry, compaction, or continuation remains. This is when the chain is walked, so fallback never competes with pi's own recovery. Any new turn in between clears the captured failure.
  4. If the failed run used tools, the rotation still happens but the retry is a continue instruction (retryInstruction), never a replay of your prompt. This avoids automatic prompt replay; the model must still respect already-completed tool work. Setting requireNoToolCalls to true skips tool-active failures entirely instead: the run stays stopped on the failed model (omp's old semantic; see below).
  5. Otherwise the original chain is resolved against available, in-scope models and its remaining untried candidates are retained across failures. New cascade routes append behind them. Fusion is the final separate tier after ordinary exhaustion/budget.
  6. If the target's context window is smaller than the one you're leaving, pi compacts before retrying.
  7. The request is re-issued on the new model: your original prompt in replay mode without tools, or retryInstruction in continue mode or after tool activity. Only actual replay requires a captured user prompt; continuing a resumed/tool-active run does not.

Already-tried models are skipped, so a failure streak walks the chain once and then stops rather than looping. The budget and the tried-set reset when a genuine new prompt arrives or a run finally settles successfully, not at an intermediate agent_end. Auth, context overflow and cancellation still take precedence over retryable symptoms, including transient internal/backend/server errors.

Quota caps are provider-wide. A failure matching providerCapPatterns — a 5-hour INFERENCE_CAP_ERROR, a billing limit, a usage ceiling — marks the entire provider as out of quota for the rest of the streak: no model behind that key is offered again, and the next configured target is chosen instead. Without this, a chain whose first target is a sibling provider keeps swapping between two capped accounts (gmail → cline-pass → gmail → …) and burns the whole budget without ever reaching your catch-all.

Glob targets resolve by family. When a glob target like neuralwatt/* matches several models, the one sharing the failed model's family is offered first — fail off cline-pass/cline-pass/glm-5.3 and the neuralwatt/* target starts at neuralwatt/glm-5.3, not whatever happens to be first in the catalogue. Exact family match > same-family variant (glm-5.3-flex) > same vendor line (glm-5.2) > catalogue order.

Multi-session safety

One pi process can host several sessions at once — subagents spawned via the Agent tool / workflows, /resume trees, RPC layers — and they all share this extension's module. All failover state is keyed by session, so two sessions failing at the same moment (say, the same provider 429 wiping out a parent and its subagent simultaneously) each get their own failover: one consumption cannot eat another's pending failure, setModel/appendEntry/the re-issued prompt all land in the session that failed, and a subagent starting or finishing never clears another session's streak or restore model.

Model resolution is also scoped: when the session has a narrowed model set (--model fixes / enabledModels settings — what /scoped-models shows), the chain only offers models inside that set, so a fallback never swaps to something the user deliberately excluded.

Tool activity is tracked per session across pi's retry attempts. agent_end fires once per provider call, and each auto-retry attempt's event carries only that attempt's error — so the flag that decides "replay the prompt" vs "send a continue instruction" is accumulated from observed tool traffic, not guessed from the failing event.

Each swap is also written to the transcript as a durable entry, so a fallback stays auditable after the fact.

Recovery protocol v1

Hosts may coordinate over the same per-session Pi event bus passed to their resource loader. No sibling import is required; ordinary/root sessions also recover without a host observer. Install host listeners before loading/binding extensions. The producer installs bus handlers at session_start and removes them at shutdown.

  • pi-fallback-chains:recovery: snapshot updates.
  • pi-fallback-chains:recovery:query: { version: 1, sessionId, reply(snapshot) }. Replies synchronously for an owned live session; no reply means absent.
  • pi-fallback-chains:recovery:cancel: { version: 1, sessionId, reason }. Invalidates the epoch, clears timers and releases pending recovery synchronously; send this before calling the SDK session's abort operation.
  • pi-fallback-chains:recovery:execution-error: { version: 1, sessionId, event, message }. Forward bindExtensions({ onError }) errors whose event is send_user_message. Pi's extension send API returns void, so asynchronous submission rejection cannot be caught by its caller.

Snapshots contain version: 1, sessionId, epoch, requestId, increasing revision, phase, pending, optional model and a sanitized category reason. Phases are idle, pending, switching, compacting, retrying, settled, cancelled, failed. Raw prompts, host reasons, and provider error messages are not published on the bus.

A lease is claimed synchronously during agent_settled, before model switching or a delay. It remains pending through compaction, submission, replacement turns and further fallback attempts. Only genuine final settlement, exhaustion/blocked recovery, submission failure or cancellation releases it. A pending recovery that settles without an assistant outcome fails explicitly with reason: "unknown-settlement" rather than retaining its lease forever; an ordinary assistant-less settlement without a lease is unchanged. Recovery timers stay referenced. Superseded generations cannot send late retries.

A host must wait for both no pending lease and an idle session before finalizing, unsubscribe only afterward, and keep cancellation/usage/turn-limit tracking active throughout. Pi's initial prompt() resolution or waitForIdle() alone does not include extension-owned recovery. This is an opt-in local protocol, not an upstream Pi completion guarantee.

Input identity limit: Pi's input event exposes source: "extension" but no issuing extension or submission ID. Input hooks can also asynchronously transform or consume the text. Consequently, extension-sourced input conservatively stays within the current recovery streak; a genuinely new request sent by another extension does not re-arm recovery after failure/cancellation. A boolean "next input is ours" or a text-match marker cannot safely distinguish overlapping, transformed, or late cancelled retries. Hosts starting a genuinely new request should use session.prompt() with its default interactive source (or rpc where appropriate). Safe independent extension re-arming needs an SDK-propagated submission identity or a separately agreed host request-boundary protocol.

Commands

| Command | Does | |---|---| | /fallback-chains | Status: config path, current model, matched chain, resolved targets, warnings | | /fallback-chains test <provider/model> | Show the chain that model would take, and what it resolves to | | /fallback-chains reload | Re-read config from disk | | /fallback-chains on\|off | Toggle for this session |

At startup the extension reports any target or chain key that matches no available model, so a typo or a removed model is visible immediately rather than at 3am:

[fallback] 2 target(s) match no available model: cline-pass/* → kilo/*, nim/*. Run /fallback-chains for details.

Migrating from oh-my-pi

Put your retry.fallbackChains block in a file and wrap or unwrap it — both shapes parse:

{ "retry": { "fallbackChains": { "cline-pass/*": ["neuralwatt/*"] }, "maxRetries": 10 } }

Differences to expect:

  • Chain keys are anchored provider globs, not arbitrary patterns. cline-pass/* means "any cline-pass model". A glob that spans providers (*) is still allowed.
  • requireNoToolCalls defaults to false, unlike omp. Tool traffic is tracked accurately across pi's retry attempts, so a tool-using failure rotates and sends a continue instruction rather than replaying your prompt — tools are still not re-run. Set it to true only if you want omp's exact behaviour of leaving such runs stopped on the failed model.
  • nim and kilo may not exist in pi — they will be reported as inert rather than failing silently.
  • Think in provider/model. A target with no provider prefix can never match, and is flagged as a warning.
  • Effort lives on the target, and also in pi's own modelThinkingLevels; both are honoured.

Changes in 0.3.0

  • Universal, disableable terminal Fusion recovery with reserved budget and stable candidate order.
  • Producer-side recovery leases, synchronous query/cancel, sanitized submission-error handling.
  • Narrow internal backend error classification; existing auth/context/abort guards retained.
  • Deterministic lifecycle/cancellation/compaction/concurrency tests, without provider calls.

Development

No build step — pi loads TypeScript via jiti, and Node 22 strips types natively for tests.

node --test test/*.test.ts   # or: npm test
node src/selfcheck.ts        # or: npm run selfcheck
npm run typecheck            # needs: npm install

Security

Extensions run with your full user permissions. Read the source before installing anything from a registry.

License

MIT