pi-provider-litellm 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  LiteLLM proxy native Provider extension for [Pi](https://pi.dev). Pi 0.99.2+ is required; on older Pi, install `npm:pi-provider-litellm@3.4.0`.
4
4
 
5
- Discovers models from self-hosted LiteLLM proxies and registers them under Pi providers. The default provider is `litellm`; optional aliases can register additional LiteLLM providers with separate credentials. Supports `/login litellm`, LiteLLM MCP tools, LiteLLM Skills Gateway prompt injection, and Google ADC token auth. Tries `/model/info` first (admin endpoint with rich metadata), falls back to `/v1/models` (OpenAI-compatible) on 401/403/404, then tries `/health` plus per-endpoint `/model/info` for older LiteLLM proxies.
5
+ Discovers models from self-hosted LiteLLM proxies and registers them under Pi providers. The default provider is `litellm`; optional aliases can register additional LiteLLM providers with separate credentials. Supports `/login litellm`, LiteLLM MCP tools, LiteLLM Skills Gateway prompt injection, and Google ADC token auth. Tries `/model/info` first (admin endpoint with rich metadata), falls back to `/v1/models` (OpenAI-compatible) on 401/403/404 or an empty list, then tries `/health` plus per-endpoint `/model/info` for older LiteLLM proxies.
6
6
 
7
7
  ## Install
8
8
 
@@ -42,11 +42,17 @@ Inside pi:
42
42
  /login litellm
43
43
  ```
44
44
 
45
- To configure an API key, run `/login`, choose `Sign in with an API key`, then choose `LiteLLM API key`. With `/login litellm`, choose `Sign in with an API key` directly.
45
+ Or for a configured alias provider (e.g. `team-a`):
46
+
47
+ ```
48
+ /login team-a
49
+ ```
50
+
51
+ To configure an API key, run `/login`, choose `Sign in with an API key`, then choose `LiteLLM API key` (or `<Provider Name> API key`). With `/login litellm`, choose `Sign in with an API key` directly; similarly, run `/login <alias>` to sign in to any configured alias directly.
46
52
 
47
53
  You'll be prompted for the base URL and API key. Credentials are persisted to `~/.pi/agent/auth.json`.
48
54
 
49
- If a proxy URL is already known — from `providers.<name>.baseUrl`, `LITELLM_BASE_URL`, or a previous login — pi offers it as the first option instead of asking you to retype it. Pick `Enter a different URL…` to point at another proxy.
55
+ If a proxy URL is already known — from `providers.<name>.baseUrl`, `LITELLM_PROVIDER_<NAME>_BASE_URL`, `LITELLM_BASE_URL`, or a previous login — pi offers it as the first option instead of asking you to retype it. Pick `Enter a different URL…` to point at another proxy.
50
56
 
51
57
  #### Enterprise SSO login
52
58
 
@@ -54,9 +60,10 @@ If your LiteLLM proxy supports SSO/OAuth authentication, Pi selects its supporte
54
60
 
55
61
  1. Run `/login litellm` inside pi and select `Sign in with LiteLLM SSO`
56
62
  2. Confirm the offered proxy URL, or enter one if this is the first login
57
- 3. Complete sign-in in the browser Pi opens, then return to Pi
63
+ 3. Without an `oidc` setting, choose `LiteLLM SSO` or your own identity provider (see [Direct OIDC login](#direct-oidc-login))
64
+ 4. Complete sign-in in the browser Pi opens, then return to Pi
58
65
 
59
- Without an `oidc` setting (see [Direct OIDC login](#direct-oidc-login)), Pi first checks `/.well-known/litellm-cli-auth`. A proxy exposing the supported CLI-auth contract uses authorization code + PKCE (`S256`) with a temporary `127.0.0.1` callback; the browser must run on the same machine as Pi. Advertised endpoints must stay on the proxy's origin, and discovery, registration, and token requests do not follow HTTP redirects. Invalid discovery stops login.
66
+ With `LiteLLM SSO`, Pi first checks `/.well-known/litellm-cli-auth`. A proxy exposing the supported CLI-auth contract uses authorization code + PKCE (`S256`) with a temporary `127.0.0.1` callback; the browser must run on the same machine as Pi. Advertised endpoints must stay on the proxy's origin, and discovery, registration, and token requests do not follow HTTP redirects. Invalid discovery stops login.
60
67
 
61
68
  Pi stores PKCE access and refresh tokens in `~/.pi/agent/auth.json` with file mode `0600`, refreshes automatically, and saves each rotated token pair. Temporary network failures or HTTP 429/5xx responses allow reuse of the existing access token only until its exact expiry. Rejected or revoked refresh credentials require `/login litellm` again.
62
69
 
@@ -76,7 +83,9 @@ Prerequisites:
76
83
  - The IdP has a public client (no client secret) registered for authorization code + PKCE with the loopback redirect `http://127.0.0.1:<port>/callback`.
77
84
  - The browser runs on the same machine as Pi.
78
85
 
79
- Configure the default `litellm` provider in the global `~/.pi/agent/settings.json`. Project settings are never read for this, so a cloned repository cannot point login at its own IdP.
86
+ To sign in without configuring anything, choose `Your identity provider (OIDC client ID)…` after confirming the proxy URL, then enter the issuer URL, the client ID, and the scopes (blank for `openid`). Pi checks them like the settings below and stores them with the credential, so the next login to the same proxy offers the same identity provider first. The login prompt does not ask for `redirectPorts`; an IdP that only accepts exact pre-registered redirect URIs needs the setting.
87
+
88
+ To skip that prompt, configure the default `litellm` provider in the global `~/.pi/agent/settings.json`. Project settings are never read for this, so a cloned repository cannot point login at its own IdP.
80
89
 
81
90
  ```json
82
91
  { "litellm": { "providers": { "litellm": {
@@ -95,7 +104,7 @@ Configure the default `litellm` provider in the global `~/.pi/agent/settings.jso
95
104
  - `scope` (default `"openid"`): must include `openid`.
96
105
  - `redirectPorts` (optional): ports tried in order, for IdPs that only accept exact pre-registered redirect URIs. Without it the OS assigns a free port.
97
106
 
98
- With `oidc` set, `Sign in with LiteLLM SSO` confirms the proxy URL and then runs only this flow: Pi reads `<issuer>/.well-known/openid-configuration`, opens the IdP's authorization page, and exchanges the code for tokens. Login sends nothing to the proxy, and IdP requests never carry `LITELLM_HEADERS` or provider `headers`. An invalid `oidc` setting fails login with a message naming the field; it never falls back to the LiteLLM flows.
107
+ With `oidc` set, `Sign in with LiteLLM SSO` confirms the proxy URL and then runs only this flow. Either way, Pi reads `<issuer>/.well-known/openid-configuration`, opens the IdP's authorization page, and exchanges the code for tokens. Login sends nothing to the proxy, and IdP requests never carry `LITELLM_HEADERS` or provider `headers`. An invalid `oidc` setting or entered value fails login with a message naming the field; it never falls back to the LiteLLM flows.
99
108
 
100
109
  The `id_token` is the bearer token. Pi checks its issuer, audience, authorized party (when present), subject, expiry, and nonce but not its signature; the proxy verifies the signature against the IdP's keys. Pi refreshes it with the refresh token, so the IdP must return a new `id_token` from the refresh grant. Some IdPs only issue a refresh token when the scope includes `offline_access`; without one, run `/login litellm` again when the `id_token` expires.
101
110
 
@@ -103,6 +112,8 @@ Pi never derives the IdP from the proxy: LiteLLM's own `/.well-known/openid-conf
103
112
 
104
113
  ### Option B — environment variables
105
114
 
115
+ #### Single provider
116
+
106
117
  ```bash
107
118
  export LITELLM_BASE_URL="https://litellm.your-domain.com"
108
119
  export LITELLM_API_KEY="sk-..."
@@ -110,9 +121,74 @@ export LITELLM_API_KEY="sk-..."
110
121
 
111
122
  Stored pi credentials for `litellm` take precedence over `LITELLM_API_KEY`; the environment key is used when no saved credential exists. `LITELLM_BASE_URL` is used when no saved login base URL exists. Chat Completions and Responses models use the proxy root plus `/v1`; native Messages uses the proxy root directly.
112
123
 
124
+ #### Multi-provider environment variables
125
+
126
+ You can configure additional LiteLLM provider aliases directly through environment variables without editing `settings.json`.
127
+
128
+ ##### Per-provider environment variables (`LITELLM_PROVIDER_<NAME>_*`)
129
+
130
+ Use the prefix `LITELLM_PROVIDER_<NAME>_` where `<NAME>` is an uppercase identifier (e.g., `TEAM_A`, `CORP_EAST`). `<NAME>` `LITELLM` is reserved for the default `litellm` provider, which reads only `LITELLM_PROVIDER_LITELLM_ENABLE_MCP` / `_MCP_ENABLED` and otherwise uses the [single-provider variables](#single-provider):
131
+
132
+ | Variable | Effect |
133
+ |---|---|
134
+ | `LITELLM_PROVIDER_<NAME>_BASE_URL` | LiteLLM proxy base URL for this provider (e.g. `https://team-a.example.com`) |
135
+ | `LITELLM_PROVIDER_<NAME>_API_KEY` | API key / bearer token for this provider |
136
+ | `LITELLM_PROVIDER_<NAME>_API_KEY_HELPER` | Shell command that prints a fresh token for this provider |
137
+ | `LITELLM_PROVIDER_<NAME>_HEADERS` | JSON string of extra request headers for this provider. Unlike `LITELLM_HEADERS`, `$VAR` and `${VAR}` inside the value are expanded, so write a literal `$` as `$$`; a reference to an unset variable drops all of this provider's headers |
138
+ | `LITELLM_PROVIDER_<NAME>_DISPLAY_NAME` | Custom display name shown in Pi UI |
139
+ | `LITELLM_PROVIDER_<NAME>_NAME` | Explicit provider ID override (e.g. `custom-name`) |
140
+ | `LITELLM_PROVIDER_<NAME>_ALLOW_INSECURE_HTTP` | Set `"1"` or `"true"` to permit plaintext HTTP for non-loopback hosts |
141
+ | `LITELLM_PROVIDER_<NAME>_USE_GCLOUD_AUTH` | Set `"1"` or `"true"` to enable Google ADC token authentication for this provider |
142
+ | `LITELLM_PROVIDER_<NAME>_ENABLE_OAUTH` | Set `"1"` or `"true"` / `"0"` or `"false"` to enable or disable LiteLLM SSO/OAuth browser login |
143
+ | `LITELLM_PROVIDER_<NAME>_ENABLE_MCP` / `_MCP_ENABLED` | Set `"0"` or `"false"` to disable MCP tool integration for this specific provider (or `"1"` / `"true"` to enable) |
144
+
145
+ ##### Bulk provider configuration
146
+
147
+ - **`LITELLM_PROVIDERS`**: Canonical comma- or whitespace-separated list of provider IDs to register (e.g., `export LITELLM_PROVIDERS="team-a, team-b, corp-east"`).
148
+ - **`LITELLM_PROVIDERS_JSON`**: Structured JSON string matching the `litellm.providers` schema in `settings.json` (e.g., `export LITELLM_PROVIDERS_JSON='{"team-a": {"baseUrl": "https://team-a.example.com", "apiKey": "sk-team-a"}}'`).
149
+
150
+ ##### Provider ID naming rules
151
+
152
+ - **Default naming:** The uppercase token `<NAME>` in `LITELLM_PROVIDER_<NAME>_*` is automatically converted to lowercase kebab-case (e.g., `TEAM_A` becomes `team-a`, `CORP_EAST` becomes `corp-east`, and `DEV` becomes `dev`).
153
+ - **Explicit override:** Set `LITELLM_PROVIDER_<NAME>_NAME` to specify an explicit provider ID (e.g., `LITELLM_PROVIDER_TEAM_A_NAME="custom-team"`).
154
+ - **Canonical IDs:** Provider IDs specified in `LITELLM_PROVIDERS`, keys in `LITELLM_PROVIDERS_JSON`, and `settings.json` aliases that follow these naming rules preserve their declared IDs and read the `LITELLM_PROVIDER_<NAME>_*` variables whose `<NAME>` is the ID uppercased with `-` read as `_`, so `team_a` and `team-a` both read `TEAM_A`. When two declared IDs share a `<NAME>`, the one listed first in `LITELLM_PROVIDERS`, then `LITELLM_PROVIDERS_JSON`, then `settings.json`, reads those variables and the other reads none.
155
+ - **Validation:** Provider IDs must match `/^[a-z0-9][a-z0-9_-]{0,63}$/` (1–64 characters, lowercase alphanumeric with `-` or `_`, starting with an alphanumeric character).
156
+ - **Reserved names:** Tool and command names (`mcp`, `skills`, `codemode`, `tool_search`, and `litellm` as an alias) cannot be used as secondary provider IDs.
157
+
158
+ ### Option C — `models.json`
159
+
160
+ For a long-lived API key, put the provider block in Pi's own `~/.pi/agent/models.json` instead of running `/login litellm`. No login is needed, and Pi injects the discovered models, so the block carries only `baseUrl` and `apiKey`:
161
+
162
+ ```json
163
+ {
164
+ "providers": {
165
+ "litellm": {
166
+ "baseUrl": "https://litellm.your-domain.com",
167
+ "apiKey": "$LITELLM_API_KEY"
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ `apiKey` is a Pi config value, so `$ENV_VAR`, `${ENV_VAR}`, `!command`, and a literal key all work, with a literal `$` escaped as `$$`. Prefer `$ENV_VAR` over a literal key: a key written literally here is exactly as readable on disk as one in `auth.json`.
174
+
175
+ Only the global `~/.pi/agent/models.json` is read. A project-level file is never consulted, so a cloned repository cannot point this provider at a proxy of its own — the same rule [Direct OIDC login](#direct-oidc-login) states for `settings.json`.
176
+
177
+ This covers long-lived tokens. SSO and other short-lived OAuth credentials still need `/login litellm`, because they require a browser round trip and refresh.
178
+
179
+ #### Precedence
180
+
181
+ A provider block may live in `models.json` or `settings.json`. The two merge per field, with `models.json` winning, and `skills`, `mcp`, and `budget` are read from `settings.json` only. `models.json` also accepts the default provider as `litellm.providers.litellm`, the same shape `settings.json` uses.
182
+
183
+ A stored `/login` credential still takes precedence over a configured `apiKey`. Pi reads the top-level `providers.litellm` block itself, though, which changes three things:
184
+
185
+ - Pi applies its `baseUrl` to every model, so a stored `/login` credential for a different proxy hides them all. To move a provider that already has a stored credential over to `models.json`, run `/logout litellm` first.
186
+ - Pi hands its `apiKey` to the provider as a credential, so that key also outranks a Google ADC token. A key in `litellm.providers.litellm` or `settings.json` ranks below ADC.
187
+ - Pi validates the fields it knows there, so `headers` must be an object. A `$LITELLM_HEADERS` string belongs in `litellm.providers.litellm`.
188
+
113
189
  ### Multiple LiteLLM provider aliases
114
190
 
115
- Add alias providers in `~/.pi/agent/settings.json` under `litellm.providers`. Each alias is registered as a separate Pi provider name, so models appear as `litellm/model-id` and `litellm-anthropic/model-id`.
191
+ Add alias providers in `~/.pi/agent/settings.json` under `litellm.providers`, or in `~/.pi/agent/models.json` under the same shape. Pi rejects a `models.json` without a top-level `providers` object, so a file that holds only aliases needs `"providers": {}` as well. Each alias is registered as a separate Pi provider name, so models appear as `litellm/model-id` and `litellm-anthropic/model-id`. In `models.json`, the default provider is usually written as the top-level `providers.litellm` shown in Option C instead.
116
192
 
117
193
  ```json
118
194
  {
@@ -157,11 +233,30 @@ Provider fields:
157
233
  | `apiKey` | `LITELLM_API_KEY_HELPER`/`LITELLM_API_KEY` for `litellm`; required for aliases | Pi config value for this provider's key. Use `$ENV_VAR`, `${ENV_VAR}`, `!command`, or a literal key. Escape a literal `$` as `$$`. |
158
234
  | `headers` | `$LITELLM_HEADERS` for `litellm`; unset for aliases | JSON string env reference or inline object of request headers |
159
235
  | `displayName` | `LITELLM_DISPLAY_NAME` / `"LiteLLM"` for `litellm`; alias name for aliases | Label shown in Pi UI |
160
- | `enabled` | `true` | Set `false` to skip an alias |
161
- | `oidc` | unset | `litellm` only. Sign in directly with an OpenID Connect identity provider instead of the LiteLLM-hosted flows; see [Direct OIDC login](#direct-oidc-login) |
236
+ | `enabled` | `true` | Set `false` to skip or disable an alias |
237
+ | `oidc` | unset | `litellm` only, in global settings. Sign in directly with an OpenID Connect identity provider instead of the LiteLLM-hosted flows; see [Direct OIDC login](#direct-oidc-login) |
162
238
  | `allowInsecureHttp` | `false` | Set `true` to permit plaintext HTTP for this provider, for example `http://host.docker.internal`. Credentials and request data will not be encrypted. Loopback HTTP works without this setting. |
239
+ | `useGcloudTokenAuth` | `false` | Aliases only. Set `true` to use Google Application Default Credentials (ADC) as this alias's bearer token. `litellm` ignores this field and uses ADC only when `LITELLM_GCLOUD_TOKEN_AUTH` is enabled. |
240
+ | `enableOAuth` | `false` | Aliases only. Set `true` to offer LiteLLM SSO browser login for this alias. `litellm` ignores this field and always offers it. |
241
+
242
+ #### Configuration & credential precedence
243
+
244
+ - **Provider definition merging precedence:**
245
+ When discovering and configuring provider aliases from multiple sources, definitions are merged in the following order:
246
+ 1. Base provider definitions from `LITELLM_PROVIDERS_JSON`
247
+ 2. Discovered environment variables (`LITELLM_PROVIDER_<NAME>_*` and canonical list in `LITELLM_PROVIDERS`)
248
+ 3. Disk settings from `~/.pi/agent/settings.json` under `litellm.providers`, merged per field with the provider blocks in `~/.pi/agent/models.json`, which win (see Option C). Disk settings override environment definitions, and setting `"enabled": false` removes or skips the provider.
249
+
250
+ Each later source overrides `displayName`, `allowInsecureHttp`, `useGcloudTokenAuth`, `enableOAuth`, and the MCP toggle. `baseUrl`, `apiKey`, and `headers` work differently: a value configured in `settings.json`, `models.json`, or `LITELLM_PROVIDERS_JSON` always wins, and `LITELLM_PROVIDER_<NAME>_BASE_URL`, `_API_KEY_HELPER`, `_API_KEY`, and `_HEADERS` apply only when neither sets that field.
163
251
 
164
- `/login litellm` and Google ADC token auth remain scoped to the default `litellm` provider. Aliases use their configured `apiKey` or manually stored auth entries matching the alias name.
252
+ - **Credential resolution precedence:**
253
+ When authenticating requests for any provider:
254
+ 1. Saved interactive credentials in `~/.pi/agent/auth.json` (from `/login <provider>` or OAuth SSO), then the `apiKey` of `models.json`'s top-level `providers.litellm`, which Pi hands to the provider as a credential
255
+ 2. Google Application Default Credentials (for an alias with `useGcloudTokenAuth` or `_USE_GCLOUD_AUTH`; for `litellm` when `LITELLM_GCLOUD_TOKEN_AUTH` is enabled)
256
+ 3. Configured `apiKey` from settings (`settings.json`, `litellm.providers` in `models.json`, or `LITELLM_PROVIDERS_JSON`, including `$ENV_VAR` and `!command`)
257
+ 4. Provider-scoped API key helper (`LITELLM_PROVIDER_<NAME>_API_KEY_HELPER`)
258
+ 5. Provider-scoped API key (`LITELLM_PROVIDER_<NAME>_API_KEY`)
259
+ 6. For the default `litellm` provider only: fallback to global `LITELLM_API_KEY_HELPER` then `LITELLM_API_KEY`.
165
260
 
166
261
  ### Optional LiteLLM features
167
262
 
@@ -191,6 +286,12 @@ LiteLLM Skills, MCP integration, and the budget footer are enabled by default. D
191
286
 
192
287
  Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false` stops the extension from registering the proxy's MCP server. Setting `budget.enabled` to `false` registers no budget footer, polls, or `/litellm-budget` command; `budget.display` is described under [Budget status](#budget-status). Restart Pi after changing these settings.
193
288
 
289
+ MCP can also be enabled or disabled per provider, though a provider setting cannot turn it on while `mcp.enabled` is `false`:
290
+ - In `~/.pi/agent/settings.json`: set `providers.<name>.mcp.enabled` (or `providers.<name>.enableMcp`) to `false` (or `true`).
291
+ - In environment variables: set `LITELLM_PROVIDER_<NAME>_ENABLE_MCP` or `LITELLM_PROVIDER_<NAME>_MCP_ENABLED` to `"0"` or `"false"` to disable MCP for a specific provider.
292
+
293
+ Restart Pi after changing these settings.
294
+
194
295
  `mcp.exposure` and `mcp.toolExposure` are passed to Pi unchanged and mean what they mean in a Pi `mcp.json` server entry: `codemode` (Pi's default when `exposure` is unset; callable from Pi's `codemode` scripts), `deferred` (declared to the model once Pi's `tool_search` loads it), `direct` (declared on every request), or `hidden`. `toolExposure` keys are tool names as LiteLLM's `/mcp` endpoint offers them, which LiteLLM prefixes with the server name, for example `github-search_code`; `*` matches any characters. Pi validates both settings, and an unusable value is reported once.
195
296
 
196
297
  Treat the configured LiteLLM proxy as trusted: Skills can add instructions to the system prompt, and MCP can expose tools the agent may call. Disable these integrations when the proxy, its administrators, or its configured content are not fully trusted.
@@ -250,6 +351,30 @@ A `/model/info` route group uses native Anthropic `/v1/messages` only when every
250
351
 
251
352
  Native Messages authenticates with `x-api-key`; every transport carries the `x-litellm-session-id` header described above. Strict JSON-schema tools are enabled separately from Messages transport and thinking compatibility: every deployment must affirm support for the actual route/API. Generic `supports_response_schema` and `supports_native_structured_output` metadata does not grant strict tools. Only deployments that LiteLLM reports with the `anthropic` adapter carry that evidence today. Claude served through Bedrock or Vertex therefore uses ordinary tool serialization, and a tool that requires strict sampling fails before the request is sent.
252
353
 
354
+ ## Image generation
355
+
356
+ Routes whose every deployment reports `mode: "image_generation"` in `/model/info` are published as Pi image models
357
+ (for example `litellm/gemini-nano-banana-2.1`). Like Pi's other image models they do not appear in `/model`; on Pi 1.0
358
+ and later, scripts reach them through Pi's `codemode` tool:
359
+
360
+ ```js
361
+ const painter = await models.getModelOfType("image", "litellm", "gemini-nano-banana-2.1");
362
+ const result = await models.generateImages(painter, {
363
+ input: [{ type: "text", text: "A red fox in the snow, watercolor" }],
364
+ });
365
+ if (result.stopReason !== "stop") return result.errorMessage;
366
+ for (const block of result.output) if (block.type === "image") image(block);
367
+ ```
368
+
369
+ Requests go to the proxy's OpenAI-compatible `/v1/images/generations` with the provider's own credential and
370
+ headers, so they work for any LiteLLM image deployment (`gpt-image-*`, `dall-e-*`, Gemini image models and others)
371
+ and are budgeted and logged by the proxy like chat requests. Usage cost comes from LiteLLM's `x-litellm-response-cost`
372
+ header when present; otherwise it is estimated from `input_cost_per_token` and `output_cost_per_image_token` (or
373
+ `output_cost_per_token`). Only routes found through `/model/info` qualify: `/v1/models` and `/health` carry no mode,
374
+ and groups that mix image and other modes, or wildcard routes, are withheld. The prompt is text only; reference images
375
+ need `/v1/images/edits`, which this provider does not call, and backends that answer with image URLs instead of
376
+ base64 data are reported as errors rather than fetched.
377
+
253
378
  ## Optional environment variables
254
379
 
255
380
  | Variable | Default | Effect |
@@ -257,10 +382,13 @@ Native Messages authenticates with `x-api-key`; every transport carries the `x-l
257
382
  | `LITELLM_API_KEY_HELPER` | unset | Command that prints a fresh LiteLLM bearer token. Takes precedence over `LITELLM_API_KEY`. The extension runs it while resolving request auth, and Pi's per-request auth path is uncached, so rotating/short-lived tokens stay fresh. |
258
383
  | `LITELLM_DISPLAY_NAME` | `LiteLLM` | Custom display name for the default `litellm` provider in the Pi UI. |
259
384
  | `LITELLM_HEADERS` | unset | JSON object of extra headers sent to LiteLLM provider, discovery, MCP, and Skills Gateway requests. Provider aliases can use it with `"headers": "$LITELLM_HEADERS"`. |
385
+ | `LITELLM_PROVIDERS` | unset | Canonical comma- or whitespace-separated list of provider IDs to register. |
386
+ | `LITELLM_PROVIDERS_JSON` | unset | JSON string of provider configuration objects matching the `litellm.providers` settings schema. |
387
+ | `LITELLM_PROVIDER_<NAME>_*` | unset | Per-provider environment variables for base URL, API key, headers, display name, OAuth, Google auth, and MCP enable/disable toggle (`_ENABLE_MCP` / `_MCP_ENABLED`). See [Multi-provider environment variables](#multi-provider-environment-variables). |
260
388
  | `LITELLM_GCLOUD_TOKEN_AUTH` | unset | If set to a non-empty value other than `0`, use Google Application Default Credentials as the LiteLLM bearer token source. This takes precedence over `LITELLM_API_KEY_HELPER` and `LITELLM_API_KEY` when no stored `/login litellm` credential exists. |
261
389
  | `GOOGLE_APPLICATION_CREDENTIALS` | Google default ADC path | Optional path to an ADC JSON file used by `LITELLM_GCLOUD_TOKEN_AUTH`. If unset, the extension checks the default gcloud ADC locations. |
262
390
  | `LITELLM_OFFLINE` | unset | If `1`, disable all model and MCP discovery, including post-login discovery; use cached models only when their stored canonical proxy root exactly matches the active credential root, including any path prefix. URL-standard host casing and default ports are canonicalized, but paths remain case-sensitive. |
263
- | `PI_OFFLINE` | unset | Pi documents it as disabling automatic network activity, including model catalog refreshes. Through Pi 1.0.3, any set value (including `0` or an empty string) disables startup model discovery and `/model` network refreshes. Run `/litellm-refresh`, or unset it, to discover; cached models can still be used offline. |
391
+ | `PI_OFFLINE` | unset | Pi documents it as disabling automatic network activity, including model catalog refreshes. Through Pi 1.2.0, any set value (including `0` or an empty string) disables startup model discovery and `/model` network refreshes. Run `/litellm-refresh`, or unset it, to discover; cached models can still be used offline. |
264
392
  | `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
265
393
  | `LITELLM_CLI_JWT_EXPIRATION_HOURS` | `24` | CLI SSO token lifetime fallback for older proxies whose poll response omits `expires_in`; mirror a non-default proxy setting locally |
266
394
  | `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model discovery (login, refresh, startup), including startup skip reasons and defaulted `/model/info` context windows. Progress messages are off by default |
@@ -291,7 +419,9 @@ LiteLLM serves the MCP servers it gateways over streamable HTTP at `/mcp`. For e
291
419
 
292
420
  The registration does not copy the provider's API key or token. It uses Pi's `auth: { provider }` setting, so Pi sends the provider's current credential as `Authorization: Bearer` on every request, and token refreshes, Google ADC, and key helpers apply without registering again. Custom headers (`LITELLM_HEADERS`, or an alias's `headers`) are registered as configured, after `$` and a leading `!` are escaped so Pi does not expand them a second time. Pi sends provider credentials only over https or to a loopback host, so a proxy reached over plain http elsewhere (`allowInsecureHttp`) gets no MCP server, and the refusal is reported once.
293
421
 
294
- The extension registers at startup and checks again before each agent turn. `/login litellm` disconnects the server as soon as it starts, before the new credential is stored, and the next turn connects again with the new credential and proxy root; a logout that leaves no root removes the server. `LITELLM_OFFLINE=1`, `LITELLM_DISCOVERY_TIMEOUT_MS=0`, and `PI_OFFLINE` register nothing. A server with the same name in your own `mcp.json` takes precedence over the extension's.
422
+ The extension registers at startup and updates the registration before each agent turn. `/login litellm` disconnects the server as soon as it starts, before the new credential is stored, and the next turn connects again with the new credential and proxy root; a logout that leaves no root removes the server. `LITELLM_OFFLINE=1`, `LITELLM_DISCOVERY_TIMEOUT_MS=0`, and `PI_OFFLINE` register nothing. A server with the same name in your own `mcp.json` takes precedence over the extension's.
423
+
424
+ Before registering, the extension sends the proxy one MCP `initialize` with the credential and headers Pi will send, and ends the session it opened. LiteLLM 1.102.0 and later refuse it with 403 when no MCP servers are granted to the key, its team, or its organization, or none of them is allowed from your IP address. The extension then registers the server disabled: Pi does not connect to it, warn about it at startup, or turn on `codemode` for it, and `/mcp` lists it as disabled. Once servers are granted, enable it in `/mcp` for the session, or restart Pi, which checks again, as does a login. When a key helper (a `!command` key) or Google ADC is configured, the check is skipped and the server is registered enabled.
295
425
 
296
426
  Pi reads the provider's credential when it sends each request, not when the server was registered. If another Pi process stores a login for a different proxy, this session's open connection can send that credential to the previous proxy until the next turn moves the server.
297
427
 
@@ -309,6 +439,11 @@ If your LiteLLM proxy exposes `/claude-code/marketplace.json`, enabled skills ar
309
439
 
310
440
  `litellm_skill_create` needs Skill Hub `sourceJson` metadata and a proxy that exposes `/claude-code/plugins`. It does not create skills from code: LiteLLM's `POST /v1/skills` is Anthropic's multipart Skills API.
311
441
 
442
+ ## PiG
443
+
444
+ A Go port for [PiG](https://github.com/MichaelKinsy/PiG) lives in [`pig/`](pig/README.md). It reads the same
445
+ settings, environment variables and `auth.json` entries; see [pig/README.md](pig/README.md) to install it.
446
+
312
447
  ## Mocked LiteLLM smoke workflow
313
448
 
314
449
  The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes route-distinct Chat, Responses, native Messages, and mixed-deployment models whose upstreams are served by VidaiMock. The smoke runner discovers those models through LiteLLM, asserts each model's expected API, exercises `/v1/chat/completions`, `/v1/responses`, and `/v1/messages`, verifies the expected `x-litellm-response-cost` behavior, and proves endpoint coverage from captured LiteLLM request logs rather than response text.
@@ -350,6 +485,8 @@ LiteLLM's `/model/info` is the metadata authority, so models.dev enrichment is a
350
485
 
351
486
  Opening `/model` refreshes configured provider catalogs in the background using Pi's native model lifecycle when network discovery is enabled.
352
487
 
488
+ Startup reuses that stored catalog instead of discovering again while the provider's entry is under 4 hours old and comes from the active proxy and the current discovery version. Interactive and RPC sessions still refresh it in the background; `pi -p` does not. For up to 4 hours after the last refresh, print mode can therefore miss a model added on the proxy or granted to your key, and after a key change on the same proxy it can list the previous key's models. Run `/litellm-refresh` or start an interactive session to update it sooner.
489
+
353
490
  Pi documents `PI_OFFLINE` and `--offline` as disabling automatic network activity, including model catalog refreshes. Pi 0.87.1 adopted that wording to resolve [earendil-works/pi#8684](https://github.com/earendil-works/pi/issues/8684), without changing the behavior. Pi still checks only whether the variable is set, so `PI_OFFLINE=0` and an empty value also disable model refreshes. With `LITELLM_VERBOSE_DISCOVERY=1`, disabled startup discovery reports its reason, for example `startup discovery skipped (PI_OFFLINE)`. To discover models while keeping `PI_OFFLINE`, run `/litellm-refresh`. To allow all discovery for one invocation, use `env -u PI_OFFLINE pi` and do not pass `--offline`. If you only want to disable Pi's version check, use `PI_SKIP_VERSION_CHECK=1` instead; it does not apply offline mode's other network restrictions. See [#155](https://github.com/balcsida/pi-provider-litellm/issues/155).
354
491
 
355
492
  ### Model host enforcement
@@ -418,7 +555,7 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
418
555
  | No models, or no new routes, while `PI_OFFLINE` is set | Pi skips every model network refresh, including the one `/model` starts. Run `/litellm-refresh`, or unset `PI_OFFLINE` (`PI_OFFLINE=0` is not enough) |
419
556
  | `No models available` at startup, gone after a restart | A Pi startup race, not discovery — see [`No models available` at startup](#no-models-available-at-startup) |
420
557
  | "discovered no models" | Proxy returned an empty list — check pi's startup log and verify `/model/info`, `/v1/models`, or `/health` responds |
421
- | `/model/info` returning 401/403/404 | Expected behavior with virtual keys — extension falls back to `/v1/models` |
558
+ | `/model/info` returning 401/403/404 or an empty list | Expected behavior with virtual keys — extension falls back to `/v1/models` |
422
559
  | Discovery times out | Increase `LITELLM_DISCOVERY_TIMEOUT_MS` or set `LITELLM_OFFLINE=1` to fall back on cached models. Offline mode does not recover a root mismatch, including a different path prefix — see [Model host enforcement](#model-host-enforcement) |
423
560
  | A provider shows no models | The base URL is missing, invalid, still the placeholder, or its full root, including any path prefix, differs from the root in the cached catalog. Check stderr and see [Model host enforcement](#model-host-enforcement) |
424
561
  | A configured or restored model fails to complete but never leaks the credential | Its `api` is absent from the current provider catalog, so Pi used global API fallback with the credential root pinned. Open `/model` against the active proxy to repopulate the protocol — see [Model host enforcement](#model-host-enforcement) |
@@ -432,7 +569,7 @@ The development probe runs against minimized snapshots with `npm run probe:proxy
432
569
  | Enterprise CLI SSO start/poll fails | Check the proxy logs and verify `/sso/cli/start` and `/sso/cli/poll/{login_id}` are reachable; only 404/405 falls back to legacy login |
433
570
  | Enterprise SSO login shows "virtual key generation failed" | The LiteLLM instance may lack a database (`/key/generate` requires one), your user account may lack key-generation permission, or the request timed out; the JWT is used directly as a fallback |
434
571
  | Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
435
- | MCP tools not showing | Run `/mcp` and check the provider's server: its state shows a connection or sign-in error. Verify the proxy serves `/mcp` and that the key has MCP access |
572
+ | MCP tools not showing | Run `/mcp` and check the provider's server: its state shows a connection or sign-in error. `disabled` means the proxy refused the key at startup, usually because no MCP servers are granted to it, its team, or its organization; after a grant, enable it in `/mcp` or restart Pi. Verify the proxy serves `/mcp` and that the key has MCP access |
436
573
  | MCP tools listed but the model cannot call them | `codemode` and `deferred` tools are reached through Pi's `codemode` and `tool_search` tools. Check that neither is disabled; `--no-extensions` disables them and Pi's MCP client alike. Setting `litellm.mcp.exposure` to `direct` declares the tools on every request instead |
437
574
  | Skills not affecting prompts | Verify the proxy exposes `/claude-code/marketplace.json` or `/v1/skills` and returns enabled skills |
438
575
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
75
75
  "es-module-lexer": "^3.0.2",
76
76
  "tsx": "^4.21.0",
77
- "typebox": "1.3.34",
77
+ "typebox": "1.3.37",
78
78
  "vitest": "^5.0.0"
79
79
  }
80
80
  }
package/src/discover.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  resolveCatalogProvider,
9
9
  routesOnlyThrough,
10
10
  } from "./backend-identity.js";
11
+ import { type DiscoveredImageModel, mapFromImageModelInfoGroup } from "./images.js";
11
12
  import {
12
13
  type CatalogResolution,
13
14
  catalogResolution,
@@ -87,7 +88,14 @@ export function normalizeBaseUrl(input: string, allowInsecureHttp = false): stri
87
88
  if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
88
89
  throw new Error("LiteLLM base URL must use HTTPS except for loopback hosts");
89
90
  }
90
- return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
91
+ return trimTrailingSlashes(input).replace(/\/v1\/?$/i, "");
92
+ }
93
+
94
+ // A loop, not /\/+$/: that regex backtracks quadratically on a long slash run followed by another character.
95
+ export function trimTrailingSlashes(value: string): string {
96
+ let end = value.length;
97
+ while (end > 0 && value[end - 1] === "/") end--;
98
+ return value.slice(0, end);
91
99
  }
92
100
 
93
101
  // Matches both the conventional `anthropic/...` prefix and aliases that
@@ -1167,9 +1175,12 @@ export async function discoverModels(
1167
1175
  const progress = options.silent ? undefined : options.onProgress;
1168
1176
  progress?.("Querying /model/info endpoint...");
1169
1177
  const infoResult = await fetchJson<ModelInfoResponse>(`${base}/model/info`, apiKey, options);
1170
- if (infoResult.ok) {
1178
+ // An empty `/model/info` list is treated like an unavailable endpoint: some proxies
1179
+ // answer 200 with no rows for keys that can still list models through `/v1/models`.
1180
+ const infoEntries = infoResult.ok ? (infoResult.data.data ?? []) : [];
1181
+ if (infoEntries.length > 0) {
1171
1182
  const groups = new Map<string, ModelInfoEntry[]>();
1172
- for (const entry of infoResult.data.data ?? []) {
1183
+ for (const entry of infoEntries) {
1173
1184
  // A route without a readable public name cannot be grouped or addressed.
1174
1185
  const route = wireString(entry.model_name);
1175
1186
  if (!route) continue;
@@ -1294,12 +1305,21 @@ export async function discoverModels(
1294
1305
  "set model_info.max_input_tokens on every deployment in each route",
1295
1306
  );
1296
1307
  }
1297
- return { source: "model_info", models, ...(proxyVersion ? { proxyVersion } : {}) };
1308
+ // Image routes are a separate operation in Pi; the chat reducer above withholds them.
1309
+ const imageModels = [...groups.entries()]
1310
+ .map(([route, group]) => mapFromImageModelInfoGroup(route, group))
1311
+ .filter((model): model is DiscoveredImageModel => model !== undefined);
1312
+ return {
1313
+ source: "model_info",
1314
+ models,
1315
+ ...(imageModels.length > 0 ? { imageModels } : {}),
1316
+ ...(proxyVersion ? { proxyVersion } : {}),
1317
+ };
1298
1318
  }
1299
- if (![401, 403, 404].includes(infoResult.status)) {
1319
+ if (!infoResult.ok && ![401, 403, 404].includes(infoResult.status)) {
1300
1320
  throw new Error(`/model/info returned ${infoResult.status}`);
1301
1321
  }
1302
- progress?.("/model/info unavailable, trying /v1/models...");
1322
+ progress?.("/model/info unavailable or empty, trying /v1/models...");
1303
1323
  const listResult = await fetchJson<ModelsListResponse>(`${base}/v1/models`, apiKey, options);
1304
1324
  if (!listResult.ok) {
1305
1325
  if ([401, 403, 404].includes(listResult.status)) {