From 3016763db4a14e8ed53494f9f42f4595ab2ef6de Mon Sep 17 00:00:00 2001 From: Alexey Volkov Date: Fri, 17 Jul 2026 16:19:20 -0700 Subject: [PATCH] feat: Support per-provider API keys and base URLs Most users do not have special AI gateways. They have separate API keys for each provider. The keys are set via standard env variables like `OPENAI_API_KEY` or `ANTHROPIC_API_KEY`. --- README.md | 5 +- .../server/src/pi/extensions/proxyProvider.ts | 91 ++++++++++++++++++- apps/server/src/pi/piAgentManager.ts | 27 +++++- 3 files changed, 112 insertions(+), 11 deletions(-) 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.", ); } }