diff --git a/README.md b/README.md index 30bfd11..38deb07 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/server/src/pi/extensions/proxyProvider.ts b/apps/server/src/pi/extensions/proxyProvider.ts index 31f025c..6bdddc7 100644 --- a/apps/server/src/pi/extensions/proxyProvider.ts +++ b/apps/server/src/pi/extensions/proxyProvider.ts @@ -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", @@ -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); + } +} diff --git a/apps/server/src/pi/piAgentManager.ts b/apps/server/src/pi/piAgentManager.ts index 500cc07..d740e12 100644 --- a/apps/server/src/pi/piAgentManager.ts +++ b/apps/server/src/pi/piAgentManager.ts @@ -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.", ); } }