pi-provider-litellm 1.2.8 → 1.2.9

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 provider extension for [Pi](https://pi.dev).
4
4
 
5
- Discovers models from a self-hosted LiteLLM proxy and registers them under the `litellm` provider. Supports `/login litellm`, `/litellm-refresh`, 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-refresh`, 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.
6
6
 
7
7
  ## Install
8
8
 
@@ -68,6 +68,57 @@ export LITELLM_API_KEY="sk-..."
68
68
 
69
69
  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.
70
70
 
71
+ ### Multiple LiteLLM provider aliases
72
+
73
+ 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`.
74
+
75
+ ```json
76
+ {
77
+ "litellm": {
78
+ "providers": {
79
+ "litellm-anthropic": {
80
+ "baseUrl": "https://litellm.your-domain.com",
81
+ "apiKey": "$LITELLM_CLAUDE_KEY",
82
+ "headers": "$LITELLM_HEADERS"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ You can also override the default provider through the same shape:
90
+
91
+ ```json
92
+ {
93
+ "litellm": {
94
+ "providers": {
95
+ "litellm": {
96
+ "baseUrl": "https://litellm.your-domain.com",
97
+ "apiKey": "$LITELLM_API_KEY",
98
+ "headers": "$LITELLM_HEADERS"
99
+ },
100
+ "litellm-anthropic": {
101
+ "baseUrl": "https://litellm.your-domain.com",
102
+ "apiKey": "$LITELLM_CLAUDE_KEY",
103
+ "headers": "$LITELLM_HEADERS"
104
+ }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Provider fields:
111
+
112
+ | Field | Default | Effect |
113
+ |---|---|---|
114
+ | `baseUrl` | `LITELLM_BASE_URL` for `litellm`; required for aliases | LiteLLM proxy URL, with or without `/v1` |
115
+ | `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 `$$`. |
116
+ | `headers` | `$LITELLM_HEADERS` for `litellm`; unset for aliases | JSON string env reference or inline object of request headers |
117
+ | `displayName` | provider name | Label shown in Pi UI |
118
+ | `enabled` | `true` | Set `false` to skip an alias |
119
+
120
+ `/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.
121
+
71
122
  ## Use
72
123
 
73
124
  ```
@@ -79,7 +130,8 @@ Stored pi credentials for `litellm` take precedence over `LITELLM_API_KEY`; the
79
130
  | Variable | Default | Effect |
80
131
  |---|---|---|
81
132
  | `LITELLM_API_KEY_HELPER` | unset | Command that prints a fresh LiteLLM bearer token. Takes precedence over `LITELLM_API_KEY`. Registered as a `!command` provider key; Pi re-runs it on every request (the per-request auth path is uncached), so rotating/short-lived tokens stay fresh. |
82
- | `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 when no stored `/login litellm` credential exists. If ADC is unavailable, the extension falls back to `LITELLM_API_KEY_HELPER` or `LITELLM_API_KEY`. |
133
+ | `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"`. |
134
+ | `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. |
83
135
  | `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. |
84
136
  | `LITELLM_OFFLINE` | unset | If `1`, skip discovery on this start; use cache only |
85
137
  | `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Discovery fetch timeout in ms; `0` to skip discovery |
@@ -96,7 +148,7 @@ export LITELLM_BASE_URL="https://litellm.your-domain.com"
96
148
  export LITELLM_GCLOUD_TOKEN_AUTH=1
97
149
  ```
98
150
 
99
- Only `authorized_user` ADC files are supported. Service account JSON files are rejected with a warning. Tokens are cached in memory for 50 minutes and are keyed by the ADC identity, so switching `GOOGLE_APPLICATION_CREDENTIALS` or changing `gcloud auth application-default login` users refreshes both request auth and model discovery cache identity. When ADC is available, the registered provider key is a Pi `!command`, so request-time auth resolves a fresh token when Pi sends model requests. If ADC cannot be read, provider registration uses the same helper or environment API key that discovery used.
151
+ Only `authorized_user` ADC files are supported. Service account JSON files are rejected with a warning. Tokens are cached in memory for 50 minutes and the registered provider key is a Pi `!command`, so request-time auth resolves a fresh token when Pi sends model requests.
100
152
 
101
153
  ## LiteLLM MCP tools
102
154
 
@@ -105,23 +157,57 @@ If your LiteLLM proxy exposes MCP REST endpoints, this extension discovers tools
105
157
  - `GET /mcp-rest/tools/list`
106
158
  - `POST /mcp-rest/tools/call`
107
159
 
108
- Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>`, with simple JSON Schema parameters mapped to Pi/TypeBox parameters. Complex schemas fall back to a single `args` object. The extension accepts LiteLLM's `tools` response shape with `inputSchema` and `mcp_info`, and executes tools with LiteLLM's `server_id`, `name`, and `arguments` fields. MCP discovery runs after successful live model discovery, `/login litellm`, or `/litellm-refresh`; it does not force network or helper-token access when a fresh model cache is used.
160
+ Each discovered tool is registered as a native Pi tool named `mcp_<server>_<tool>`, with simple JSON Schema parameters mapped to Pi/TypeBox parameters. Complex schemas fall back to a single `args` object. MCP discovery runs after successful live model discovery, `/login litellm`, or `/litellm-refresh`; it does not force network or helper-token access when a fresh model cache is used. MCP tools run in Pi's parallel tool mode and retry transient failures once.
109
161
 
110
- ## LiteLLM Skills Gateway
162
+ ## LiteLLM Skill Hub
111
163
 
112
- If your LiteLLM proxy exposes `/v1/skills`, enabled skills are fetched before each agent turn and appended to the system prompt as a `litellm_skills` section. The extension also registers Pi tools for basic Skills Gateway management:
164
+ If your LiteLLM proxy exposes `/claude-code/marketplace.json`, enabled skills are fetched before each agent turn and appended to the system prompt as a `litellm_skills` section. The extension falls back to the legacy `/v1/skills` Skills Gateway path when Skill Hub is unavailable. It also registers Pi tools for basic skill management:
113
165
 
114
166
  - `litellm_skill_list`
115
167
  - `litellm_skill_create`
116
168
  - `litellm_skill_delete`
117
169
 
170
+ ## Mocked LiteLLM smoke workflow
171
+
172
+ The `LiteLLM Smoke` GitHub Actions workflow starts VidaiMock and a real LiteLLM proxy on the runner. LiteLLM exposes OpenAI-compatible and Anthropic routes whose upstreams are served by VidaiMock, then this extension's smoke runner discovers those models through LiteLLM and sends `/v1/chat/completions` requests through the proxy.
173
+
174
+ This keeps the LiteLLM integration path under test but does not call real LLM APIs. No provider API keys or GitHub Models permission are required. The smoke runner also asserts that discovery came from `/model/info` (`LITELLM_SMOKE_EXPECT_SOURCE`) so a silent fallback to `/v1/models` fails the run. The workflow also runs auth checks plus optional Postgres-backed auth checks when `LITELLM_LICENSE` is configured for virtual-key and admin-route behavior, then runs a non-interactive Pi CLI smoke with `--list-models` and `-p` against both the OpenAI-compatible and Anthropic-backed routes, so extension loading, model discovery, and real completion paths are covered without opening the TUI.
175
+
176
+ ## Development
177
+
178
+ This package requires Node.js `>=22.19.0`. CI currently uses Node `24.16.0`.
179
+
180
+ ```bash
181
+ npm ci
182
+ npm run check
183
+ npm run clean && npm run build
184
+ ```
185
+
186
+ `npm run check` runs Biome, type checking, and the Vitest suite. Runtime changes must be built before local Pi smoke checks because the extension entrypoint is `./dist/index.js`.
187
+
188
+ Before changing package contents or dependency policy, also run:
189
+
190
+ ```bash
191
+ npm run supply-chain:guard
192
+ npm pack --dry-run
193
+ ```
194
+
195
+ The published npm package should contain only `dist`, `README.md`, and `LICENSE`.
196
+
197
+ ## Release
198
+
199
+ Releases are driven by semver tags named `v*.*.*`. The GitHub release workflow installs from the lockfile, runs the checks, builds `dist`, verifies the package tarball, publishes to npm with provenance, and creates a GitHub release.
200
+
201
+ Before tagging a release, keep `package.json` and `package-lock.json` versions in sync and verify the dry-run package contents.
202
+
118
203
  ## Slash commands
119
204
 
120
- - `/litellm-refresh` — force re-fetch the model list, ignoring cache
205
+ - `/litellm-refresh` — force re-fetch the model list for all configured LiteLLM providers, ignoring cache
206
+ - `/litellm-refresh <provider>` — refresh one configured provider alias, for example `/litellm-refresh litellm-anthropic`
121
207
 
122
208
  ## Cache
123
209
 
124
- The model list is cached at `~/.pi/agent/litellm-models.json` with the base URL and a keyed fingerprint of the selected auth source. Changing the base URL, API key, helper command, or Google ADC identity invalidates the cache automatically.
210
+ The default provider model list is cached at `~/.pi/agent/litellm-models.json` with a keyed fingerprint of the base URL + API key. Alias provider caches use `~/.pi/agent/litellm-models-<provider>.json`. Changing the base URL or key invalidates that provider's cache automatically.
125
211
 
126
212
  If the cache is older than 24 hours, the extension refreshes it in the background on session start (non-blocking). Failures are silent — the cached models remain in use. Run `/litellm-refresh` to force an immediate refresh.
127
213
 
@@ -138,7 +224,7 @@ If the cache is older than 24 hours, the extension refreshes it in the backgroun
138
224
  | 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 |
139
225
  | Enterprise SSO token prompt fails with "SSO token is required" | The token field was left empty — paste the token copied from the LiteLLM UI |
140
226
  | MCP tools not showing | Verify the proxy exposes `/mcp-rest/tools/list` and run `/litellm-refresh` after fixing the proxy |
141
- | Skills not affecting prompts | Verify the proxy exposes `/v1/skills` and returns enabled skills |
227
+ | Skills not affecting prompts | Verify the proxy exposes `/claude-code/marketplace.json` or `/v1/skills` and returns enabled skills |
142
228
 
143
229
  ## License
144
230
 
package/dist/cost.d.ts CHANGED
@@ -5,4 +5,8 @@ export interface ModelCostInfo {
5
5
  cacheReadCostPerToken: number;
6
6
  cacheWriteCostPerToken: number;
7
7
  }
8
- export declare function setupLiteLLMCostTracking(pi: ExtensionAPI, models: ProviderModelConfig[]): (models: ProviderModelConfig[]) => void;
8
+ export interface ProviderModels {
9
+ provider: string;
10
+ models: ProviderModelConfig[];
11
+ }
12
+ export declare function setupLiteLLMCostTracking(pi: ExtensionAPI, providerModels: ProviderModels[]): (providerModels: ProviderModels[]) => void;
package/dist/cost.js CHANGED
Binary file
@@ -2,6 +2,7 @@ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
2
  import type { DiscoveryOptions, DiscoveryResult } from "./types.js";
3
3
  export declare function normalizeBaseUrl(input: string): string;
4
4
  export declare function isMoonshotModel(modelId: string): boolean;
5
+ export declare function isGpt55Model(modelId: string): boolean;
5
6
  export declare function shouldSuppressReasoningContent(modelId: string): boolean;
6
7
  export declare function buildCompat(modelId: string): ProviderModelConfig["compat"];
7
8
  export declare function withTimeout(timeoutMs: number, signal?: AbortSignal): {
package/dist/discover.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getModels, getProviders } from "@earendil-works/pi-ai";
1
+ import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
2
2
  const DEFAULT_TIMEOUT_MS = 5000;
3
3
  const DEFAULT_CONTEXT_WINDOW = 128_000;
4
4
  const DEFAULT_MAX_TOKENS = 16_384;
@@ -8,6 +8,13 @@ let modelsDevCatalog;
8
8
  export function normalizeBaseUrl(input) {
9
9
  return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
10
10
  }
11
+ const RESPONSES_MODE_PATTERN = /^responses?$/i;
12
+ function isResponsesMode(mode) {
13
+ return mode !== undefined && RESPONSES_MODE_PATTERN.test(mode);
14
+ }
15
+ function isChatStyleMode(mode) {
16
+ return mode === undefined || mode === "chat" || isResponsesMode(mode);
17
+ }
11
18
  // Matches both the conventional `anthropic/...` prefix and aliases that
12
19
  // LiteLLM deployments commonly assign to Anthropic-backed routes (e.g.
13
20
  // `google/claude-sonnet-4-6`, `opus-4.7`, `sonnet-4.6`, `haiku-4.5`). Without
@@ -16,9 +23,16 @@ export function normalizeBaseUrl(input) {
16
23
  const ANTHROPIC_MODEL_PATTERN = /(?:^|[-_/.:])(?:anthropic\/|(?:claude|opus|sonnet|haiku)(?=$|[-_/.:]))/i;
17
24
  const MOONSHOT_MODEL_PATTERN = /^(moonshotai\/|moonshot\/|kimi[-/])/i;
18
25
  const FORCED_THINKING_MODEL_PATTERN = /(?:^|[-/])thinking(?:[-/]|$)/i;
26
+ // Deployments expose the gpt-5.5 route under varying names (`llm-gateway/gpt-5.5`,
27
+ // bare `gpt-5.5`, dated ids like `gpt-5.5-20260504143601`); match them all so the
28
+ // tool+reasoning workaround survives route renames.
29
+ const GPT55_MODEL_PATTERN = /(?:^|\/)gpt-5\.5(?:$|[-.])/i;
19
30
  export function isMoonshotModel(modelId) {
20
31
  return MOONSHOT_MODEL_PATTERN.test(modelId);
21
32
  }
33
+ export function isGpt55Model(modelId) {
34
+ return GPT55_MODEL_PATTERN.test(modelId);
35
+ }
22
36
  export function shouldSuppressReasoningContent(modelId) {
23
37
  return isMoonshotModel(modelId) && !FORCED_THINKING_MODEL_PATTERN.test(modelId);
24
38
  }
@@ -129,7 +143,7 @@ async function fetchJson(url, apiKey, options) {
129
143
  const { signal, cancel } = withTimeout(timeoutMs, options.signal);
130
144
  try {
131
145
  const response = await fetch(url, {
132
- headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
146
+ headers: { ...options.headers, Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
133
147
  signal,
134
148
  });
135
149
  if (!response.ok)
@@ -199,8 +213,9 @@ function mapFromModelInfo(entry) {
199
213
  if (!id)
200
214
  return undefined;
201
215
  const info = entry.model_info ?? {};
202
- if (info.mode && info.mode !== "chat")
216
+ if (!isChatStyleMode(info.mode))
203
217
  return undefined;
218
+ const responsesMode = isResponsesMode(info.mode);
204
219
  const catalogModel = findCatalogModel(id);
205
220
  return {
206
221
  id,
@@ -211,15 +226,17 @@ function mapFromModelInfo(entry) {
211
226
  contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
212
227
  maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
213
228
  compat: buildCompat(id),
229
+ ...(responsesMode ? { api: "openai-responses" } : {}),
214
230
  };
215
231
  }
216
232
  function mapFromHealthModelInfo(entry, fallbackId) {
217
233
  const model = mapFromModelInfo(entry);
218
234
  if (model || !fallbackId)
219
235
  return model;
220
- if (entry.model_info?.mode && entry.model_info.mode !== "chat")
236
+ if (!isChatStyleMode(entry.model_info?.mode))
221
237
  return undefined;
222
238
  const info = entry.model_info ?? {};
239
+ const responsesMode = isResponsesMode(info.mode);
223
240
  const catalogModel = findCatalogModel(fallbackId);
224
241
  return {
225
242
  id: fallbackId,
@@ -230,6 +247,7 @@ function mapFromHealthModelInfo(entry, fallbackId) {
230
247
  contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
231
248
  maxTokens: info.max_output_tokens ?? DEFAULT_MAX_TOKENS,
232
249
  compat: buildCompat(fallbackId),
250
+ ...(responsesMode ? { api: "openai-responses" } : {}),
233
251
  };
234
252
  }
235
253
  function mapFromHealthEndpoint(entry) {