pi-provider-litellm 2.0.4 → 2.0.6

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
@@ -137,6 +137,8 @@ LiteLLM Skills and MCP integration are enabled by default. Disable either featur
137
137
 
138
138
  Setting `skills.enabled` to `false` disables the Skills Gateway management tools, skill fetching, and system-prompt injection. Setting `mcp.enabled` to `false` disables LiteLLM MCP discovery and tool registration. Restart Pi after changing these settings so previously registered tools are removed.
139
139
 
140
+ 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.
141
+
140
142
  ## Use
141
143
 
142
144
  ```
@@ -156,6 +158,8 @@ Setting `skills.enabled` to `false` disables the Skills Gateway management tools
156
158
  | `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup); discovery is silent by default |
157
159
  | `LITELLM_MODELS_DEV` | enabled | Set to `0` to disable models.dev metadata enrichment, including its cache and network request; `/v1/models` still uses Pi catalog metadata and defaults |
158
160
 
161
+ Only use a trusted `LITELLM_API_KEY_HELPER` or `!command`. Prefer an absolute executable path, keep secrets out of command arguments, and print only the token to stdout without logging it to stderr.
162
+
159
163
  `LITELLM_DISCOVERY_TIMEOUT_MS=0` disables automatic and explicit refresh model discovery. It does not replace the base URL or API key settings required to send requests when you are not using `/login litellm`.
160
164
 
161
165
  Models.dev metadata is cached in `litellm-models-dev.json` under the Pi agent directory for 28 days. Fresh data avoids the public request; stale data is used immediately while one background refresh updates the cache. Set `LITELLM_MODELS_DEV=0` when your LiteLLM metadata is authoritative and no external enrichment is wanted.
@@ -1,9 +1,11 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
1
2
  import type { DiscoveredModel, DiscoveryOptions, DiscoveryResult } from "./types.js";
2
3
  export declare function normalizeBaseUrl(input: string): string;
3
4
  export declare function isMoonshotModel(modelId: string): boolean;
4
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): DiscoveredModel["compat"];
8
+ export declare function enrichCachedModel(model: Model<Api>): Model<Api>;
7
9
  export declare function discoverModels(baseUrl: string, apiKey: string, options?: DiscoveryOptions & {
8
10
  onProgress?: (message: string) => void;
9
11
  silent?: boolean;
package/dist/discover.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { isIP } from "node:net";
2
3
  import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
3
4
  import { writeJsonAtomic } from "./cache.js";
4
5
  const DEFAULT_TIMEOUT_MS = 5000;
@@ -10,6 +11,12 @@ const MODELS_DEV_CACHE_TTL_MS = 28 * 24 * 60 * 60 * 1000;
10
11
  const modelsDevCaches = new Map();
11
12
  const modelsDevRefreshes = new Map();
12
13
  export function normalizeBaseUrl(input) {
14
+ const url = new URL(input);
15
+ const hostname = url.hostname.toLowerCase();
16
+ const loopback = hostname === "localhost" || hostname === "[::1]" || (isIP(hostname) === 4 && hostname.startsWith("127."));
17
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
18
+ throw new Error("LiteLLM base URL must use HTTPS except for loopback hosts");
19
+ }
13
20
  return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
14
21
  }
15
22
  const RESPONSES_MODE_PATTERN = /^responses?$/i;
@@ -77,6 +84,36 @@ function findCatalogModel(id, ownedBy) {
77
84
  }
78
85
  return undefined;
79
86
  }
87
+ export function enrichCachedModel(model) {
88
+ // ponytail: legacy cache lacks field provenance; add per-field cache provenance if strict preservation becomes necessary.
89
+ if (!model.name.endsWith(" (no metadata)") ||
90
+ model.reasoning ||
91
+ model.thinkingLevelMap !== undefined ||
92
+ model.input.length !== 1 ||
93
+ model.input[0] !== "text" ||
94
+ model.cost.input !== 0 ||
95
+ model.cost.output !== 0 ||
96
+ model.cost.cacheRead !== 0 ||
97
+ model.cost.cacheWrite !== 0 ||
98
+ model.cost.tiers !== undefined ||
99
+ model.contextWindow !== DEFAULT_CONTEXT_WINDOW ||
100
+ model.maxTokens !== DEFAULT_MAX_TOKENS) {
101
+ return model;
102
+ }
103
+ const catalogModel = findCatalogModel(model.id);
104
+ if (!catalogModel)
105
+ return model;
106
+ return {
107
+ ...model,
108
+ name: catalogModel.name,
109
+ reasoning: catalogModel.reasoning,
110
+ thinkingLevelMap: catalogModel.thinkingLevelMap,
111
+ input: catalogModel.input,
112
+ cost: catalogModel.cost,
113
+ contextWindow: catalogModel.contextWindow,
114
+ maxTokens: catalogModel.maxTokens,
115
+ };
116
+ }
80
117
  function catalogLookupIds(id) {
81
118
  const lookupIds = new Set([id]);
82
119
  const unprefixed = id.includes("/") ? id.slice(id.indexOf("/") + 1) : id;
@@ -325,6 +362,21 @@ function mapModelsDevMetadata(model) {
325
362
  }
326
363
  return metadata;
327
364
  }
365
+ function mapReasoningEfforts(info) {
366
+ const flags = [
367
+ ["off", "none", info.supports_none_reasoning_effort],
368
+ ["minimal", "minimal", info.supports_minimal_reasoning_effort],
369
+ ["low", "low", info.supports_low_reasoning_effort],
370
+ ["medium", "medium", info.supports_medium_reasoning_effort],
371
+ ["high", "high", info.supports_high_reasoning_effort],
372
+ ["xhigh", "xhigh", info.supports_xhigh_reasoning_effort],
373
+ ["max", "max", info.supports_max_reasoning_effort],
374
+ ];
375
+ const map = Object.fromEntries(flags
376
+ .filter(([, , supported]) => supported !== undefined)
377
+ .map(([level, value, supported]) => [level, supported ? value : null]));
378
+ return Object.keys(map).length > 0 ? map : undefined;
379
+ }
328
380
  function mapFromModelInfo(entry) {
329
381
  const id = entry.model_name;
330
382
  if (!id)
@@ -334,11 +386,15 @@ function mapFromModelInfo(entry) {
334
386
  return undefined;
335
387
  const responsesMode = isResponsesMode(info.mode);
336
388
  const catalogModel = findCatalogModel(id);
389
+ const reasoningEffortMap = mapReasoningEfforts(info);
390
+ const thinkingLevelMap = catalogModel?.thinkingLevelMap || reasoningEffortMap
391
+ ? { ...catalogModel?.thinkingLevelMap, ...reasoningEffortMap }
392
+ : undefined;
337
393
  return {
338
394
  id,
339
395
  name: id,
340
396
  reasoning: info.supports_reasoning ?? false,
341
- ...(catalogModel?.thinkingLevelMap ? { thinkingLevelMap: catalogModel.thinkingLevelMap } : {}),
397
+ ...(thinkingLevelMap ? { thinkingLevelMap } : {}),
342
398
  input: info.supports_vision ? ["text", "image"] : ["text"],
343
399
  cost: mapModelInfoCost(info, catalogModel?.cost),
344
400
  contextWindow: info.max_input_tokens ?? DEFAULT_CONTEXT_WINDOW,
@@ -85,7 +85,7 @@ async function exchangeRefreshToken(credentials) {
85
85
  signal: AbortSignal.timeout(10_000),
86
86
  });
87
87
  if (!response.ok) {
88
- console.warn(`LiteLLM gcloud auth: Token exchange failed (${response.status}): ${await response.text()}`);
88
+ console.warn(`LiteLLM gcloud auth: Token exchange failed (${response.status})`);
89
89
  return null;
90
90
  }
91
91
  const data = (await response.json());
package/dist/index.js CHANGED
@@ -140,8 +140,7 @@ async function generateVirtualKey(baseUrl, userToken, signal, headers) {
140
140
  signal: boundedSignal,
141
141
  });
142
142
  if (!response.ok) {
143
- const text = await response.text().catch(() => "");
144
- throw new Error(`Virtual key generation failed (${response.status}): ${text}`);
143
+ throw new Error(`Virtual key generation failed (${response.status})`);
145
144
  }
146
145
  const data = (await response.json());
147
146
  if (typeof data.key !== "string" || !data.key)
@@ -948,11 +947,7 @@ export default async function (pi) {
948
947
  context.credential) {
949
948
  const auth = await authForCredential(definition, context.credential);
950
949
  defaultRuntimeAuth = auth;
951
- const registration = registerMcpTools(auth, context.signal);
952
- if (context.signal)
953
- await registration;
954
- else
955
- void registration.catch(() => undefined);
950
+ void registerMcpTools(auth, context.signal).catch(() => undefined);
956
951
  }
957
952
  }
958
953
  };
package/dist/provider.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createProvider } from "@earendil-works/pi-ai";
2
2
  import { openAICompletionsApi, openAIResponsesApi } from "@earendil-works/pi-ai/compat";
3
+ import { enrichCachedModel } from "./discover.js";
3
4
  export function toNativeModels(provider, baseUrl, models) {
4
5
  return models.map((model) => ({
5
6
  ...model,
@@ -9,7 +10,7 @@ export function toNativeModels(provider, baseUrl, models) {
9
10
  }));
10
11
  }
11
12
  export function createLiteLLMProvider(options) {
12
- return createProvider({
13
+ const provider = createProvider({
13
14
  id: options.id,
14
15
  name: options.name,
15
16
  baseUrl: options.baseUrl,
@@ -26,4 +27,14 @@ export function createLiteLLMProvider(options) {
26
27
  "openai-responses": openAIResponsesApi(),
27
28
  },
28
29
  });
30
+ const refreshModels = provider.refreshModels;
31
+ if (!refreshModels)
32
+ return provider;
33
+ return {
34
+ ...provider,
35
+ refreshModels: (context) => refreshModels({
36
+ ...context,
37
+ stored: context.stored && { ...context.stored, models: context.stored.models.map(enrichCachedModel) },
38
+ }),
39
+ };
29
40
  }
package/dist/types.d.ts CHANGED
@@ -31,6 +31,13 @@ export interface ModelInfoEntry {
31
31
  max_input_tokens?: number;
32
32
  max_output_tokens?: number;
33
33
  supports_reasoning?: boolean;
34
+ supports_none_reasoning_effort?: boolean;
35
+ supports_minimal_reasoning_effort?: boolean;
36
+ supports_low_reasoning_effort?: boolean;
37
+ supports_medium_reasoning_effort?: boolean;
38
+ supports_high_reasoning_effort?: boolean;
39
+ supports_xhigh_reasoning_effort?: boolean;
40
+ supports_max_reasoning_effort?: boolean;
34
41
  supports_vision?: boolean;
35
42
  };
36
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -58,15 +58,24 @@
58
58
  "@earendil-works/pi-ai": ">=0.81.0",
59
59
  "@earendil-works/pi-coding-agent": ">=0.81.0"
60
60
  },
61
+ "peerDependenciesMeta": {
62
+ "@earendil-works/pi-ai": {
63
+ "optional": true
64
+ },
65
+ "@earendil-works/pi-coding-agent": {
66
+ "optional": true
67
+ }
68
+ },
61
69
  "overrides": {
62
70
  "basic-ftp": "6.0.1",
63
- "fast-xml-builder": "1.2.0",
64
- "protobufjs": "8.7.1"
71
+ "nanoid": "3.3.18",
72
+ "protobufjs": "8.7.1",
73
+ "undici": "8.9.0"
65
74
  },
66
75
  "devDependencies": {
67
76
  "@biomejs/biome": "^2.4.15",
68
- "@earendil-works/pi-ai": "^0.83.0",
69
- "@earendil-works/pi-coding-agent": "^0.83.0",
77
+ "@earendil-works/pi-ai": "^0.84.2",
78
+ "@earendil-works/pi-coding-agent": "^0.84.2",
70
79
  "@kitlangton/terminal-control": "0.6.0",
71
80
  "@types/node": "^26.0.0",
72
81
  "@typescript/native-preview": "7.0.0-dev.20260707.2",