Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ workflow — research, code generation, document building, scheduled automation,

- **Node.js** (the version matching the Docker base image, currently 24+)
- **pnpm** `10.28.0` (the pinned package manager)
- The **`pi`** agent binary on your `PATH` (or set `PI_BIN` to its location)
- Access to an LLM proxy for `pi` to call (see `PI_PROXY_URL` / `PI_PROXY_API_KEY` below)
- The **`pi`** agent binary on your `PATH` (or set `PI_BIN`; `start_local.sh` installs it for you)
- LLM access for `pi`: a provider key such as `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`, or an
LLM gateway via `PI_PROXY_URL` / `PI_PROXY_API_KEY` (see below)

### Install and run

Expand Down
91 changes: 86 additions & 5 deletions apps/server/src/pi/extensions/proxyProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,34 @@
* is therefore excluded from our type-check (`@ts-nocheck`) and is never
* imported by the server itself — only passed as a path to the Pi subprocess,
* which loads it with jiti.
*
* Two modes, selected by whether an LLM gateway is configured:
*
* - Gateway — when both `PI_PROXY_URL` and `PI_PROXY_API_KEY` are set, every
* provider is routed through that single proxy via its per-vendor sub-paths
* (the Shopify llm-gateway layout). This is the deployed default.
* - Direct — when either is missing, each provider is registered straight
* against its own public API using the well-known key/base-URL env vars
* (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …). This lets a plain checkout talk
* to OpenAI/Anthropic without a gateway (see `start_local.sh`).
*/

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

// Trailing slash is stripped so per-provider paths concatenate cleanly.
const PROXY_URL = (process.env.PI_PROXY_URL ?? "").replace(/\/+$/, "");
const stripSlash = (url: string): string => url.replace(/\/+$/, "");

export default function (pi: ExtensionAPI) {
// Managed routes mirror the local shopify-proxy extension so selecting a
// built-in model "just works" through the proxy.
const PROXY_URL = stripSlash(process.env.PI_PROXY_URL ?? "");
const PROXY_API_KEY = process.env.PI_PROXY_API_KEY ?? "";

// A gateway needs both an endpoint and its credential; without either we fall
// back to talking to each provider's public API directly.
const useGateway = PROXY_URL !== "" && PROXY_API_KEY !== "";

/**
* Registers the gateway routes — one proxy origin with a per-vendor sub-path per
* provider — so selecting any built-in model "just works" through the proxy.
*/
function registerGatewayProviders(pi: ExtensionAPI): void {
pi.registerProvider("anthropic", {
baseUrl: `${PROXY_URL}/apis/anthropic`,
apiKey: "$PI_PROXY_API_KEY",
Expand All @@ -43,3 +61,66 @@ export default function (pi: ExtensionAPI) {
apiKey: "$PI_PROXY_API_KEY",
});
}

/**
* Registers only the providers whose native API key is present, each against its
* own public endpoint. Base URLs follow the suffix convention Pi expects and the
* gateway routes mirror: the OpenAI-compatible base includes `/v1`; the
* Anthropic base does not (Pi appends `/v1/messages`). The `$VAR` apiKey is
* resolved from the environment by Pi at request time.
*/
function registerDirectProviders(pi: ExtensionAPI): void {
if (process.env.OPENAI_API_KEY) {
pi.registerProvider("openai", {
baseUrl: stripSlash(
process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
),
apiKey: "$OPENAI_API_KEY",
});
}

if (process.env.ANTHROPIC_API_KEY) {
pi.registerProvider("anthropic", {
baseUrl: stripSlash(
process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
),
apiKey: "$ANTHROPIC_API_KEY",
});
}

if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) {
pi.registerProvider("google", {
baseUrl: stripSlash(
process.env.GOOGLE_BASE_URL ??
"https://generativelanguage.googleapis.com/v1beta/openai",
),
apiKey: process.env.GEMINI_API_KEY
? "$GEMINI_API_KEY"
: "$GOOGLE_API_KEY",
});
}

if (process.env.GROQ_API_KEY) {
pi.registerProvider("groq", {
baseUrl: stripSlash(
process.env.GROQ_BASE_URL ?? "https://api.groq.com/openai/v1",
),
apiKey: "$GROQ_API_KEY",
});
}

if (process.env.XAI_API_KEY) {
pi.registerProvider("xai", {
baseUrl: stripSlash(process.env.XAI_BASE_URL ?? "https://api.x.ai/v1"),
apiKey: "$XAI_API_KEY",
});
}
}

export default function (pi: ExtensionAPI) {
if (useGateway) {
registerGatewayProviders(pi);
} else {
registerDirectProviders(pi);
}
}
27 changes: 23 additions & 4 deletions apps/server/src/pi/piAgentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,37 @@ function buildPiArgs(
return args;
}

/** Warns once-per-spawn when the LLM proxy env vars are unset. */
/**
* Whether a native provider key is set, meaning the proxy-provider extension
* runs in direct mode and targets a provider's public API rather than a gateway.
*/
function hasDirectProviderKey(): boolean {
return Boolean(
process.env.OPENAI_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY,
);
}

/**
* Warns once-per-spawn when Pi has no usable LLM credentials. In direct mode the
* gateway env vars are irrelevant, so their absence is not worth warning about.
*/
function warnMissingProxyEnv(): void {
if (hasDirectProviderKey()) return;

if (!process.env.PI_PROXY_API_KEY) {
console.warn(
"[pi] PI_PROXY_API_KEY is not set; Pi will fail to reach the LLM gateway. " +
"Run `export PI_PROXY_API_KEY=$(devx llm-gateway print-token --key)` before starting the server.",
"[pi] No LLM credentials found: set a provider key (e.g. OPENAI_API_KEY or " +
"ANTHROPIC_API_KEY) to use a provider directly, or PI_PROXY_API_KEY to use " +
"an LLM gateway. Without one, Pi cannot reach a model.",
);
}
if (!process.env.PI_PROXY_URL) {
console.warn(
`[pi] PI_PROXY_URL is not set; the proxy-provider extension will default to ${PI_PROXY_URL}. ` +
"Set PI_PROXY_URL to point Pi at a different LLM proxy.",
"Set PI_PROXY_URL to point Pi at an LLM gateway, or set a provider key for direct access.",
);
}
}
Expand Down
Loading