pi-sidecar
A standalone HTTP service that wraps the
Pi coding agent SDK
(requires @earendil-works/pi-coding-agent ≥ 0.99.0), exposing AI sessions over a
simple JSON API. Ships with a Python client for easy integration.
Features
- Session management — create, prompt, abort, and delete AI sessions over REST; optionally supply a provider-agnostic, session-scoped API key
- Model discovery — discover models from ACPX, CLI, and built-in providers. See key-scoped model discovery for user keys.
- Provider discovery —
GET /providerslists every provider registered in the initialized runtime, including built-ins and extensions, even without discoverable models or ambient credentials. The JSON response is{ "providers": [{ "provider": string, "supportsSessionApiKey": boolean }] }.provideris the exact ID;supportsSessionApiKeysays whetherPOST /sessionsacceptsapi_key, not whether the server has credentials. No credentials or auth diagnostics are returned. Python:await SidecarClient.get_providers()returns a typed list of these records. Use discovery to enumerate providers,GET /modelsfor available models, andGET /models/:provider/statusfor details about a known provider. - Provider diagnostics —
GET /models/:provider/statusreports registration, model count, auth status, andsupportsSessionApiKey(a boolean indicating whetherPOST /sessionsacceptsapi_keyfor that provider, regardless of server credentials; false for unknown, headless-excluded, and ambient CLI/ACPX providers). Python:SidecarClient.get_model_provider_status(). Full auth detail on loopback; on non-loopback binds (SIDECAR_HOST/DEV_MODE)authStatus/authCheckare redacted to{ configured }/{ type };supportsSessionApiKeyremains public on both binds and in 404 responses - Custom tools — plug in domain-specific tools at session creation via
custom_tools - HTTP-backed tools — custom tools with
httpconfig get automatic request execution with parameter interpolation and security hardening - Subagent delegation — delegate tasks to specialized agents via the
subagenttool (loaded as a Pi SDK extension) - Watchdog — opt-in health-check poller for companion backend liveness
- Localhost-only — binds to
127.0.0.1by default; no auth needed behind the network boundary
Packages
| Package | Language | Install |
|---|---|---|
@myk-org/pi-sidecar |
TypeScript | npm install @myk-org/pi-sidecar |
pi-sidecar-client |
Python ≥ 3.10 | uv pip install pi-sidecar-client |
The npm tarball is the Node server only. Python wheels/sdists publish to PyPI — they must not ship
inside @myk-org/pi-sidecar even though uv build writes them to the same dist/ as tsc.
Quick Start
from pi_sidecar_client import call_ai_once
result = await call_ai_once(
"Summarize this log file",
ai_provider="gemini",
ai_model="gemini-2.5-flash",
system_prompt="You are a log analyst.",
)
print(result.text)
To override the server's stored/environment API key for one session, pass
api_key=user_api_key to SidecarClient.create_session, call_ai, or
call_ai_once. The wire field is api_key in POST /sessions (a non-empty
string of at most 1,024 characters); the response remains { "session_id": "..." }. The key applies only
to the selected provider and that session, takes precedence over server
credentials, and is held in memory until the session is deleted or expires.
Omitting it preserves server credential fallback. Providers without Pi API-key
auth capability reject it with HTTP 400; OAuth, Vertex/ADC and CLI login state
are not configured by this field. Keys are not saved in session files, returned
in API responses, or passed to nested agents. Treat sidecar access as privileged:
its default loopback bind is not an authentication boundary against local users.
# Start the sidecar
npm run build && node dist/server.js # listens on 127.0.0.1:9100
# Or, for local development (background by default; default port 9201 — override with SIDECAR_PORT; see --help):
scripts/start-sidecar.sh
See the Consumer Integration Guide for Docker, Python client, and deployment best practices. See the full documentation for everything else.
Key-scoped model discovery
GET /models uses the server's credentials. Static getModels metadata
cannot verify what a supplied key can access. For providers with
supportsSessionApiKey: true in GET /providers, query with the key instead:
curl -s -X POST http://127.0.0.1:9100/models/for-api-key \
-H 'Content-Type: application/json' \
-d '{"provider":"openai","api_key":"<your-key>"}'
# Returns: {"models": [...], "modelListingSupported": true}
The response is { "models": [...], "modelListingSupported": boolean }.
A provider with native key-scoped listing returns modelListingSupported: true;
models: [] then means the listing returned no models. A provider without
native key-scoped listing returns { "models": [], "modelListingSupported": false }.
In that case, ask the user to enter a model ID. A canonical builtin always
supports key-scoped listing on its own host, even when its catalog mixes APIs
(OpenRouter fronts Anthropic models alongside OpenAI ones); a models.json
baseUrl override or a custom gateway is still refused, so a request-supplied
endpoint never receives the credential — unless the admin flagged that custom
provider trustKeyScopedListing in the agent-dir models.json (see
Custom providers and settings (agent dir)) —
and for such a flagged provider, models is the union of the live listing
and the provider's manifest-configured models[] from models.json (see the
trustKeyScopedListing
section). A listing is only key-listed:
it does not verify that a prompt will work. Google results require
generateContent; known non-generation OpenAI model families are excluded,
but remaining IDs are not prompt-verified. Do not use static catalog entries
as verified models for that key. Known static IDs can be used with a supplied
key. Unknown IDs can create a session only when the native listing supplies
reliable positive input and output token limits (currently Google); otherwise
POST /sessions returns HTTP 400 for missing model metadata.
In Python, await SidecarClient.get_models_for_api_key(provider, api_key)
returns the full dictionary, including models and modelListingSupported.
Providers without supportsSessionApiKey return HTTP 400. The lookup is
request-local: the sidecar does not cache the supplied key or result.
Custom providers and settings (agent dir)
By default the sidecar's internal runtime keeps its catalog in the ephemeral,
non-configurable scratch dir /tmp/pi-sidecar-agent. Deployments that need to
register custom pi providers or supply pi settings point the sidecar at a
persistent agent dir instead:
# Environment variable (honored by npx pi-sidecar / pi-sidecar-start / node dist/server.js)
export PI_SIDECAR_AGENT_DIR=/etc/pi-sidecar/agent
npx pi-sidecar
// Or programmatically
import { startSidecar } from "@myk-org/pi-sidecar";
startSidecar({ agentDir: "/etc/pi-sidecar/agent" });
Precedence: explicit agentDir option → PI_SIDECAR_AGENT_DIR (trimmed,
non-empty) → /tmp/pi-sidecar-agent. Resolution is logged at startup. Use an
absolute path — a relative path resolves against the sidecar process's cwd,
which differs between the CLI entry and embedded startSidecar() callers.
Files honored from the configured dir (read once at process start — restart the sidecar after changing them):
| File | Effect |
|---|---|
models.json |
Custom pi providers registered on the shared runtime — listed by GET /models and GET /providers, usable in POST /sessions; providers flagged trustKeyScopedListing also serve POST /models/for-api-key |
auth.json |
Provider credentials for the shared runtime (same format as interactive pi's auth.json) |
settings.json |
Seeded into the in-memory settings store for the internal runtime and every session (e.g. defaultProvider) |
A models.json apiKey may reference an environment variable (e.g.
"$ENMAAS_API_KEY") — the variable must exist in the sidecar process's
environment (pass it through in Docker with -e / Env:). Worked example —
an EnMaaS OpenAI-compatible gateway at /etc/pi-sidecar/agent/models.json:
{
"providers": {
"enmaas": {
"baseUrl": "https://enmaas.example.com/v1",
"api": "openai-completions",
"apiKey": "$ENMAAS_API_KEY",
"models": [
{ "id": "enmaas/gpt-4o", "name": "GPT-4o via EnMaaS" }
]
}
}
}
export PI_SIDECAR_AGENT_DIR=/etc/pi-sidecar/agent
export ENMAAS_API_KEY=<your-key>
npx pi-sidecar
# GET /models now lists enmaas/gpt-4o (provider "enmaas");
# POST /sessions accepts {"provider": "enmaas", "model": "enmaas/gpt-4o", ...}
Notes:
- The per-request
agent_dirfield onPOST /sessionsstill overrides only the session's resource loading (skills/prompts/agents) — it never reconfigures the shared runtime. - There is no REST API change:
/health,/models,/providers, and/sessionskeep their semantics; the configured dir only changes what the runtime is seeded with at startup. - Settings seeded from
settings.jsonnever re-enable compaction — sidecar sessions are short-lived and always run with compaction disabled.
Key-scoped listing for custom providers (trustKeyScopedListing)
By default POST /models/for-api-key refuses to send a request-scoped (user)
key to any host that is not a provider-owned canonical builtin endpoint or a
literal loopback address, so custom gateways return
{"models": [], "modelListingSupported": false}. An admin can opt a single
custom provider into key-scoped model listing by flagging it in the agent
dir's models.json (read once at startup — restart the sidecar to apply):
{
"providers": {
"enmaas": {
"baseUrl": "https://enmaas.example.com/v1",
"api": "openai-completions",
"apiKey": "$ENMAAS_API_KEY",
"trustKeyScopedListing": true,
"models": [
{ "id": "enmaas/gpt-4o", "name": "GPT-4o via EnMaaS" }
]
}
}
}
A flagged provider's listing request goes to exactly its configured baseUrl
— the same host the manifest's server credential (apiKey) already goes to.
The flag is honored only from this admin-controlled manifest; request- or
session-level input can never set it, and unflagged providers keep the default
refusal. URL hygiene is still enforced (no userinfo, query, or hash; only
http:/https:), and the provider must declare at least one model with all
models sharing the provider baseUrl (the existing uniformity check). The
extra field is ignored by pi's provider registration, so it does not affect
GET /models or POST /sessions.
For a flagged provider whose native listing succeeds, POST /models/for-api-key returns the union of the live gateway listing and the
provider's manifest-configured models[] (snapshot at startup, same file
read as the flag). Real case: an EnMaaS gateway lists 15 models via
/v1/models but not the manually configured rits/zai-org/glm-5-3 — the
response contains both. Manifest-only models are appended after the live
entries in the same response shape (provider/id/name/source, plus
capabilities.inputTokenLimit/capabilities.outputTokenLimit when configured —
the models.json contextWindow/maxTokens mapped onto the same schema live
entries use). On an
id collision the manifest entry wins — the admin explicitly configured it —
replacing the live entry's name and metadata at the live position. The union
applies only to that provider's own listing; one provider's manifest models
never appear in another's response. When the native listing fails or is
refused (modelListingSupported: false), no union is applied.
Every entry carries a source provenance discriminator so consumers can
distinguish verified from admin-declared models: "listing" means the
gateway listed the id for this key — the id is verified for the credential
(this includes manifest-wins collision entries: the gateway did list the id,
and the manifest only supplied the name and token limits). "manifest"
means the entry is admin-declared in models.json only and was not
verified for this key — a user with a model-restricted key may not be able to
use it, so consumers must not treat it as accessible until access is confirmed
with that key.
A key-listed model used in POST /sessions with an api_key needs token
limits: when the live listing omits them, the sidecar falls back to the
manifest-declared contextWindow/maxTokens for that id, and otherwise
rejects the session with a 400 naming the models.json remedy — limits are
never fabricated for an unknown model.
CLI Commands
After installing @myk-org/pi-sidecar, two CLI commands are available:
# Start the sidecar server (Node.js entry point)
npx pi-sidecar
# Start/stop via shell script (background mode, dev defaults on port 9201)
npx pi-sidecar-start
npx pi-sidecar-start --stop
npx pi-sidecar-start --help
License
Apache-2.0
Metadata
Release files for pi-sidecar-client 4.9.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pi_sidecar_client-4.9.0.tar.gz | 25.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pi_sidecar_client-4.9.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.0 kB
Release files / pi_sidecar_client-4.9.0.tar.gz
| Download URL | pi_sidecar_client-4.9.0.tar.gz |
|---|---|
| Size | 25.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d77dc8f943f7934801f3f6e29c1a8d2246d66452288e370d9cf213be2485453c
|
|
BLAKE2b-256 checksum How to use checksums |
0abfd68043933df91338e37685bea4e3475f0e06db94ca57f3b1de164c8d7246
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / pi_sidecar_client-4.9.0-py3-none-any.whl
| Download URL | pi_sidecar_client-4.9.0-py3-none-any.whl |
|---|---|
| Size | 19.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ac2b6167d8d04249f02fa9a8b15a6d0d00fc35d4afcb28f8a9e5a290d32550d8
|
|
BLAKE2b-256 checksum How to use checksums |
fcad0274425835e49e8f536d69b6689916ad60efa8962f729e3ab9d59f8169bb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|