fix(coding-agent): make config env references explicit

closes #5095
This commit is contained in:
Armin Ronacher
2026-05-28 11:57:10 +02:00
parent 5b31ffd744
commit 3e9f717445
17 changed files with 930 additions and 71 deletions

View File

@@ -43,7 +43,7 @@ export default function (pi: ExtensionAPI) {
pi.registerProvider("my-provider", {
name: "My Provider",
baseUrl: "https://api.example.com",
apiKey: "MY_API_KEY",
apiKey: "$MY_API_KEY",
api: "openai-completions",
models: [
{
@@ -83,7 +83,7 @@ pi.registerProvider("openai", {
pi.registerProvider("google", {
baseUrl: "https://ai-gateway.corp.com/google",
headers: {
"X-Corp-Auth": "CORP_AUTH_TOKEN" // env var or literal
"X-Corp-Auth": "$CORP_AUTH_TOKEN" // env var or literal
}
});
```
@@ -112,7 +112,7 @@ export default async function (pi: ExtensionAPI) {
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "LOCAL_OPENAI_API_KEY",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
@@ -132,7 +132,7 @@ This registers the fetched models before startup finishes.
```typescript
pi.registerProvider("my-llm", {
baseUrl: "https://api.my-llm.com/v1",
apiKey: "MY_LLM_API_KEY", // env var name or literal value
apiKey: "$MY_LLM_API_KEY", // env var reference
api: "openai-completions", // which streaming API to use
models: [
{
@@ -155,6 +155,8 @@ pi.registerProvider("my-llm", {
When `models` is provided, it **replaces** all existing models for that provider.
`apiKey` and custom header values use the same config value syntax as `models.json`: `!command` at the start executes a command for the whole value, `$ENV_VAR` and `${ENV_VAR}` interpolate environment variables, `$$` emits a literal `$`, and `$!` emits a literal `!`.
## Unregister Provider
Use `pi.unregisterProvider(name)` to remove a provider that was previously registered via `pi.registerProvider(name, ...)`:
@@ -163,7 +165,7 @@ Use `pi.unregisterProvider(name)` to remove a provider that was previously regis
// Register
pi.registerProvider("my-llm", {
baseUrl: "https://api.my-llm.com/v1",
apiKey: "MY_LLM_API_KEY",
apiKey: "$MY_LLM_API_KEY",
api: "openai-completions",
models: [
{
@@ -243,7 +245,7 @@ If your provider expects `Authorization: Bearer <key>` but doesn't use a standar
```typescript
pi.registerProvider("custom-api", {
baseUrl: "https://api.example.com",
apiKey: "MY_API_KEY",
apiKey: "$MY_API_KEY",
authHeader: true, // adds Authorization: Bearer header
api: "openai-completions",
models: [...]
@@ -592,7 +594,7 @@ Register your stream function:
```typescript
pi.registerProvider("my-provider", {
baseUrl: "https://api.example.com",
apiKey: "MY_API_KEY",
apiKey: "$MY_API_KEY",
api: "my-custom-api",
models: [...],
streamSimple: streamMyProvider
@@ -629,7 +631,7 @@ interface ProviderConfig {
/** API endpoint URL. Required when defining models. */
baseUrl?: string;
/** API key or environment variable name. Required when defining models (unless oauth). */
/** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or !command. Required when defining models (unless oauth). */
apiKey?: string;
/** API type for streaming. Required at provider or model level when defining models. */
@@ -642,7 +644,7 @@ interface ProviderConfig {
options?: SimpleStreamOptions
) => AssistantMessageEventStream;
/** Custom headers to include in requests. Values can be env var names. */
/** Custom headers to include in requests. Values use the same resolution syntax as apiKey. */
headers?: Record<string, string>;
/** If true, adds Authorization: Bearer header with the resolved API key. */

View File

@@ -199,7 +199,7 @@ export default async function (pi: ExtensionAPI) {
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "LOCAL_OPENAI_API_KEY",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
@@ -1555,7 +1555,7 @@ If you need to discover models from a remote endpoint, prefer an async extension
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "PROXY_API_KEY", // env var name or literal
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
@@ -1602,7 +1602,7 @@ pi.registerProvider("corporate-ai", {
**Config options:**
- `name` - Display name for the provider in UI such as `/login`.
- `baseUrl` - API endpoint URL. Required when defining models.
- `apiKey` - API key or environment variable name. Required when defining models (unless `oauth` provided).
- `apiKey` - API key literal, environment interpolation (`$ENV_VAR` or `${ENV_VAR}`), or leading `!command`. Required when defining models (unless `oauth` provided). `$$` escapes `$`, and `$!` escapes a literal `!` without triggering command execution.
- `api` - API type: `"anthropic-messages"`, `"openai-completions"`, `"openai-responses"`, etc.
- `headers` - Custom headers to include in requests.
- `authHeader` - If true, adds `Authorization: Bearer` header automatically.

View File

@@ -101,7 +101,7 @@ Use `google-generative-ai` with a `baseUrl` to add models from Google AI Studio,
"my-google": {
"baseUrl": "https://generativelanguage.googleapis.com/v1beta",
"api": "google-generative-ai",
"apiKey": "GEMINI_API_KEY",
"apiKey": "$GEMINI_API_KEY",
"models": [
{
"id": "gemma-4-31b-it",
@@ -143,22 +143,31 @@ Set `api` at provider level (default for all models) or model level (override pe
### Value Resolution
The `apiKey` and `headers` fields support three formats:
The `apiKey` and `headers` fields support command execution, environment interpolation, and literals:
- **Shell command:** `"!command"` executes and uses stdout
- **Shell command:** `"!command"` at the start executes the whole value as a command and uses stdout
```json
"apiKey": "!security find-generic-password -ws 'anthropic'"
"apiKey": "!op read 'op://vault/item/credential'"
```
- **Environment variable:** Uses the value of the named variable
- **Environment interpolation:** `"$ENV_VAR"` or `"${ENV_VAR}"` uses the value of the named variable. Interpolation works inside larger literals.
```json
"apiKey": "MY_API_KEY"
"apiKey": "$MY_API_KEY"
"apiKey": "${KEY_PREFIX}_${KEY_SUFFIX}"
```
`$FOO_BAR` is the variable `FOO_BAR`; use `${FOO}_BAR` when `BAR` is literal text. Missing environment variables make the value unresolved.
- **Escapes:** `"$$"` emits a literal `"$"`; `"$!"` emits a literal `"!"` without triggering command execution.
```json
"apiKey": "$$literal-dollar-prefix"
"apiKey": "$!literal-bang-prefix"
```
- **Literal value:** Used directly
```json
"apiKey": "sk-..."
```
Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup.
For `models.json`, shell commands are resolved at request time. pi intentionally does not apply built-in TTL, stale reuse, or recovery logic for arbitrary commands. Different commands need different caching and failure strategies, and pi cannot infer the right one.
If your command is slow, expensive, rate-limited, or should keep using a previous value on transient failures, wrap it in your own script or command that implements the caching or TTL behavior you want.
@@ -172,10 +181,10 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
"providers": {
"custom-proxy": {
"baseUrl": "https://proxy.example.com/v1",
"apiKey": "MY_API_KEY",
"apiKey": "$MY_API_KEY",
"api": "anthropic-messages",
"headers": {
"x-portkey-api-key": "PORTKEY_API_KEY",
"x-portkey-api-key": "$PORTKEY_API_KEY",
"x-secret": "!op read 'op://vault/item/secret'"
},
"models": [...]
@@ -268,7 +277,7 @@ To merge custom models into a built-in provider, include the `models` array:
"providers": {
"anthropic": {
"baseUrl": "https://my-proxy.example.com/v1",
"apiKey": "ANTHROPIC_API_KEY",
"apiKey": "$ANTHROPIC_API_KEY",
"api": "anthropic-messages",
"models": [...]
}
@@ -327,7 +336,7 @@ Some Anthropic models require adaptive thinking (`thinking.type: "adaptive"` plu
"anthropic-proxy": {
"baseUrl": "https://proxy.example.com",
"api": "anthropic-messages",
"apiKey": "ANTHROPIC_PROXY_KEY",
"apiKey": "$ANTHROPIC_PROXY_KEY",
"compat": {
"supportsEagerToolInputStreaming": false,
"supportsLongCacheRetention": true,
@@ -405,7 +414,7 @@ Example:
"providers": {
"openrouter": {
"baseUrl": "https://openrouter.ai/api/v1",
"apiKey": "OPENROUTER_API_KEY",
"apiKey": "$OPENROUTER_API_KEY",
"api": "openai-completions",
"models": [
{
@@ -455,7 +464,7 @@ Vercel AI Gateway example:
"providers": {
"vercel-ai-gateway": {
"baseUrl": "https://ai-gateway.vercel.sh/v1",
"apiKey": "AI_GATEWAY_API_KEY",
"apiKey": "$AI_GATEWAY_API_KEY",
"api": "openai-completions",
"models": [
{

View File

@@ -101,23 +101,31 @@ The file is created with `0600` permissions (user read/write only). Auth file cr
### Key Resolution
The `key` field supports three formats:
The `key` field supports command execution, environment interpolation, and literals:
- **Shell command:** `"!command"` executes and uses stdout (cached for process lifetime)
- **Shell command:** `"!command"` at the start executes the whole value as a command and uses stdout (cached for process lifetime)
```json
{ "type": "api_key", "key": "!security find-generic-password -ws 'anthropic'" }
{ "type": "api_key", "key": "!op read 'op://vault/item/credential'" }
```
- **Environment variable:** Uses the value of the named variable
- **Environment interpolation:** `"$ENV_VAR"` or `"${ENV_VAR}"` uses the value of the named variable. Interpolation works inside larger literals.
```json
{ "type": "api_key", "key": "MY_ANTHROPIC_KEY" }
{ "type": "api_key", "key": "$MY_ANTHROPIC_KEY" }
{ "type": "api_key", "key": "${KEY_PREFIX}_${KEY_SUFFIX}" }
```
`$FOO_BAR` is the variable `FOO_BAR`; use `${FOO}_BAR` when `BAR` is literal text. Missing environment variables make the value unresolved.
- **Escapes:** `"$$"` emits a literal `"$"`; `"$!"` emits a literal `"!"` without triggering command execution.
```json
{ "type": "api_key", "key": "$$literal-dollar-prefix" }
{ "type": "api_key", "key": "$!literal-bang-prefix" }
```
- **Literal value:** Used directly
```json
{ "type": "api_key", "key": "sk-ant-..." }
{ "type": "api_key", "key": "public" }
```
OAuth credentials are also stored here after `/login` and managed automatically.
Legacy uppercase env-var-like values such as `MY_API_KEY` are migrated to `$MY_API_KEY` on startup. OAuth credentials are also stored here after `/login` and managed automatically.
## Cloud Providers