pi-provider-proxy
Per-provider HTTP proxy routing for pi. Turn a proxy on or off for each provider (and pick a different proxy per provider) at runtime, without touching pi's global httpProxy.
Package details
Install pi-provider-proxy from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-provider-proxy- Package
pi-provider-proxy- Version
0.1.1- Published
- Nov 8, 2026
- Downloads
- 236/mo · 236/wk
- Author
- laboon85
- License
- MIT
- Types
- extension
- Size
- 199.6 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./dist/index.js"
],
"changelogPath": "./CHANGELOG.md"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-provider-proxy
Per-provider HTTP proxy routing for pi.
Turn the proxy on or off for each provider, give a single provider its own proxy, and switch at
runtime with /proxy — no restart, no editing settings.json, no patching fetch.
proxy · provider-a→p:7890
The line is quiet by design: direct rules are not advertised (they are the "nothing special"
case), and statusLine: "auto" only renders when a provider is actually proxied, the extension is
switched off, or the dispatcher failed to install.
Why
pi has one global proxy knob (httpProxy in ~/.pi/agent/settings.json, or HTTPS_PROXY in the
environment) plus NO_PROXY. That is all-or-nothing, per host at best, and it is read once at
startup. Real setups need per-provider decisions:
- One provider's endpoint is only reachable through a local proxy (restricted network, geo block, or a gateway that terminates TLS itself).
- Another provider's endpoint is faster — or only reachable — direct.
- A corporate or self-hosted provider must go through a different, dedicated proxy.
- The decision differs per machine and changes during a session, so it should not require editing
settings.jsonand restarting.
pi-provider-proxy adds exactly that layer. It installs a routing
undici Dispatcher as pi's global dispatcher and
decides per request origin whether the request goes direct, through a specific proxy, or
"inherit" (pi's own behaviour).
It does not replace pi's providers, models or credentials, and it does not bundle a second copy of undici — it drives the instance pi already loaded.
Install
pi install npm:pi-provider-proxy
From a local checkout (loaded from that path, nothing is copied):
git clone https://github.com/Lineance/pi-provider-proxy.git
cd pi-provider-proxy && npm install && npm run build
pi install ./pi-provider-proxy # or: pi -e ./pi-provider-proxy (one run only)
Requires pi with Node 22+ and an ESM loader (any current pi release).
Restart pi. Run /proxy to confirm the dispatcher is installed:
pi-provider-proxy — enabled
dispatcher: installed (undici …/pi-coding-agent/node_modules/undici/index.js)
default: inherit
config: ~/.pi/agent/pi-provider-proxy.json
providers:
provider-a proxy via http://127.0.0.1:7890 → proxy http://127.0.0.1:7890 [api.example.com]
status line: auto (direct rules hidden)
Quick start
/proxy provider-a proxy http://127.0.0.1:7890
/proxy provider-b direct
/proxy test provider-a
/proxy why api.example.com
Rules are written to ~/.pi/agent/pi-provider-proxy.json and take effect on the next request.
Commands
| Command | What it does |
|---|---|
/proxy |
Overview: install state, default rule, every provider with its effective route, config problems, counters |
/proxy on · /proxy off |
Master switch. off makes everything inherit pi's settings (persisted) |
/proxy <provider> direct |
Force a provider to connect directly, even if a global proxy is configured |
/proxy <provider> proxy [url] |
Route a provider through a proxy (defaults to default.proxy) |
/proxy <provider> inherit |
Remove the provider rule (back to default) |
/proxy default <mode> [url] |
Rule for hosts that match no provider |
/proxy host <host> <mode> [url] |
Rule for a raw host (api.x.test, *.x.test, *) — escape hatch |
/proxy host <host> clear |
Remove a host rule |
/proxy test <provider> |
Dial the provider's baseUrl through the active route and report status + latency |
/proxy why <host|provider> |
Explain which rule wins and why (includes provider, hosts, NO_PROXY interplay) |
/proxy stats |
Request counters (direct / proxy / inherit / errors) |
/proxy status |
Show status-line settings and a live preview |
/proxy status auto·always·off |
When the footer line renders (auto: only when something is proxied, disabled or broken) |
/proxy status direct on·off |
Whether direct rules are listed in the footer (default: hidden) |
/proxy reload |
Re-read config and the session's provider catalogue |
/proxy help |
Usage |
Add --project (-p) to any mutating command to write <cwd>/.pi/pi-provider-proxy.json instead of
the global file.
Configuration
Two layers, later wins: ~/.pi/agent/pi-provider-proxy.json, then <cwd>/.pi/pi-provider-proxy.json
(project layer only when the project is trusted).
{
"$schema": "https://unpkg.com/pi-provider-proxy/schema.json",
"enabled": true,
"default": "inherit",
"providers": {
"provider-a": { "mode": "proxy" }, // use default.proxy
"provider-b": "direct", // shorthand string form
"corp-llm": { "mode": "proxy", "proxy": "http://10.0.0.9:3128" },
},
"hosts": {
"*.internal.test": "direct",
},
"loopbackAlwaysDirect": true,
"statusLine": "auto",
"statusLineShowDirect": false,
}
Modes
| Mode | Meaning |
|---|---|
inherit |
Hand the request to pi's own dispatcher. settings.httpProxy, HTTP_PROXY, HTTPS_PROXY and NO_PROXY behave exactly as they do without this extension. |
direct |
Always connect directly. A dedicated agent is used, so a global httpProxy cannot leak in. |
proxy |
Always use the rule's proxy URL (or default.proxy if the rule has none). |
Precedence: enabled: false → private/loopback guard → hosts rule (exact, then longest *. match,
then *) → providers rule → default → inherit.
Fields
| Field | Type | Default | Notes |
|---|---|---|---|
enabled |
boolean | true |
Master switch; false = installed but inert |
default |
rule | "inherit" |
Applied when no provider/host rule matches |
providers |
map | {} |
Keyed by pi provider id (as shown by /proxy or /model) |
hosts |
map | {} |
Raw host rules: api.x.test, *.x.test, * |
loopbackAlwaysDirect |
boolean | true |
Keeps loopback / link-local / RFC1918 / .local off the proxy |
statusLine |
auto | always | off |
auto |
Footer summary; auto shows it only when something is proxied, disabled or broken |
statusLineShowDirect |
boolean | false |
Also list direct / inherit rules in the footer |
Proxy URLs must be http:// or https://. undici's ProxyAgent does not speak SOCKS; point the
rule at your client's mixed/HTTP port instead (Clash/mihomo mixed-port is HTTP, e.g. 7890).
Recipes
Provider that needs a proxy, everything else direct
{ "default": "direct", "providers": { "provider-a": { "mode": "proxy", "proxy": "http://127.0.0.1:7890" } } }
Proxy by default, but AI endpoints direct
{
"default": { "mode": "proxy", "proxy": "http://127.0.0.1:7890" },
"providers": { "provider-a": "direct", "provider-b": "direct" },
}
Keep pi's httpProxy and only exempt one provider
Leave default at inherit and set the one exemption:
/proxy provider-b direct
inherit keeps NO_PROXY/httpProxy semantics for everything else, so the two mechanisms compose
instead of conflict.
Temporary bypass
/proxy off # everything inherits pi's settings again
/proxy on
Interaction with pi's own proxy settings
settings.httpProxyis applied by pi asHTTP_PROXY/HTTPS_PROXYfor pi-managed HTTP clients.inheritroutes use exactly that dispatcher, unchanged, including its idle/body timeouts.- Timers for
directandproxyagents followhttpIdleTimeoutMsfrom pi's settings (default 5 min). NO_PROXYis not re-implemented here:inheritdelegates,directbypasses everything,proxyuses the given proxy unconditionally.- WebSocket transports (
transport: "websocket"providers such as Codex Responses) do not go through undici dispatchers. This extension cannot route them;/proxyprints a note when it sees such a provider.
Safety and failure behaviour
| Situation | Behaviour |
|---|---|
| undici cannot be resolved | Nothing is installed; one warning; /proxy shows the diagnostics and every attempted location |
| Invalid config | Problems are listed with their JSON path; valid parts still apply |
| Proxy URL invalid / agent creation fails | That request falls back to inherit, an error is counted, one warning |
| Another extension or pi replaces the dispatcher | Re-installed on the next lifecycle event (self-healing) |
PI_PROVIDER_PROXY_DISABLE=1 |
Never installs (CI, bisecting) |
PI_PROVIDER_PROXY_DEBUG=1 |
Per-request dispatch host route lines on stderr |
PI_PROVIDER_PROXY_UNDICI=/abs/path/undici/index.js |
Explicit undici instance (Nix, exotic layouts) |
The extension never bundles or imports its own undici copy — see
docs/architecture.md for why that matters.
Troubleshooting
dispatcher: FAILED — Could not resolve the undici instance pi uses
Set PI_PROVIDER_PROXY_UNDICI to the undici/index.js inside your pi installation, and include the
/proxy output when reporting.
Provider still times out although the rule says proxy
Run /proxy test <provider> and /proxy why <host>. If the host is loopback/private, the guard keeps
it direct — set "loopbackAlwaysDirect": false if you really want to proxy it.
inherit goes through a proxy you did not configure
That is pi's httpProxy/HTTPS_PROXY/NO_PROXY at work (or another extension's dispatcher). Use
direct for that provider to bypass it unconditionally.
Everything is direct after /proxy off
off means "stop making decisions" — pi's own settings then apply as before. Use
/proxy <provider> direct instead.
Development
npm install
npm run verify # typecheck → lint → format:check → coverage → build → publint/attw → size guard
npm test
See docs/architecture.md for the routing model and the reason a
dispatcher-level implementation beats patching fetch.
Layout: src/core/* is the routing engine (pure functions + dispatcher, no pi imports),
src/extension/* is the pi integration (state, /proxy, footer), test/fixtures/* provides a fake
undici plus a real CONNECT proxy for the socket-level integration test.
License
MIT