pi-provider-litellm 2.1.0 → 2.2.1

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
@@ -115,6 +115,7 @@ Provider fields:
115
115
  | `headers` | `$LITELLM_HEADERS` for `litellm`; unset for aliases | JSON string env reference or inline object of request headers |
116
116
  | `displayName` | provider name | Label shown in Pi UI |
117
117
  | `enabled` | `true` | Set `false` to skip an alias |
118
+ | `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. |
118
119
 
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.
120
121
 
@@ -157,14 +158,11 @@ Treat the configured LiteLLM proxy as trusted: Skills can add instructions to th
157
158
  | `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
158
159
  | `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 |
159
160
  | `LITELLM_VERBOSE_DISCOVERY` | unset | If `1`, enable progress messages during model and MCP discovery (login, refresh, startup); discovery is silent by default |
160
- | `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 |
161
161
 
162
162
  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.
163
163
 
164
164
  `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`.
165
165
 
166
- 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.
167
-
168
166
  ### Google ADC token auth
169
167
 
170
168
  When your LiteLLM proxy accepts Google OAuth access tokens, you can let the extension refresh tokens from Application Default Credentials:
@@ -1,6 +1,6 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { DiscoveredModel, DiscoveryOptions, DiscoveryResult } from "./types.js";
3
- export declare function normalizeBaseUrl(input: string): string;
3
+ export declare function normalizeBaseUrl(input: string, allowInsecureHttp?: boolean): string;
4
4
  export declare function isMoonshotModel(modelId: string): boolean;
5
5
  export declare function isGpt55Model(modelId: string): boolean;
6
6
  export declare function shouldSuppressReasoningContent(modelId: string): boolean;
package/dist/discover.js CHANGED
@@ -1,20 +1,14 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { isIP } from "node:net";
3
2
  import { getModels, getProviders } from "@earendil-works/pi-ai/compat";
4
- import { writeJsonAtomic } from "./cache.js";
5
3
  const DEFAULT_TIMEOUT_MS = 5000;
6
4
  const DEFAULT_CONTEXT_WINDOW = 128_000;
7
5
  const DEFAULT_MAX_TOKENS = 16_384;
8
6
  const KNOWN_PROVIDER_SET = new Set(getProviders());
9
- const MODELS_DEV_URL = "https://models.dev/api.json";
10
- const MODELS_DEV_CACHE_TTL_MS = 28 * 24 * 60 * 60 * 1000;
11
- const modelsDevCaches = new Map();
12
- const modelsDevRefreshes = new Map();
13
- export function normalizeBaseUrl(input) {
7
+ export function normalizeBaseUrl(input, allowInsecureHttp = false) {
14
8
  const url = new URL(input);
15
9
  const hostname = url.hostname.toLowerCase();
16
10
  const loopback = hostname === "localhost" || hostname === "[::1]" || (isIP(hostname) === 4 && hostname.startsWith("127."));
17
- if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
11
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
18
12
  throw new Error("LiteLLM base URL must use HTTPS except for loopback hosts");
19
13
  }
20
14
  return input.replace(/\/+$/, "").replace(/\/v1\/?$/i, "");
@@ -150,42 +144,6 @@ function mapModelInfoCost(info, fallback) {
150
144
  ...(fallback?.tiers ? { tiers: fallback.tiers } : {}),
151
145
  };
152
146
  }
153
- function getFallbackProviderAndModel(id, ownedBy) {
154
- const [prefix, ...rest] = id.split("/");
155
- const prefixProvider = toKnownProvider(prefix);
156
- if (prefixProvider && rest.length > 0) {
157
- return { provider: prefixProvider, modelId: rest.join("/") };
158
- }
159
- return { provider: toKnownProvider(ownedBy), modelId: id };
160
- }
161
- function findModelsDevModel(catalog, id, ownedBy) {
162
- const { provider, modelId } = getFallbackProviderAndModel(id, ownedBy);
163
- if (!provider)
164
- return undefined;
165
- const models = catalog?.[provider]?.models;
166
- return models && Object.hasOwn(models, modelId) ? models[modelId] : undefined;
167
- }
168
- function awaitWithSignal(promise, signal) {
169
- if (!signal)
170
- return promise;
171
- if (signal.aborted)
172
- return Promise.reject(signal.reason);
173
- return new Promise((resolve, reject) => {
174
- const cleanup = () => signal.removeEventListener("abort", onAbort);
175
- const onAbort = () => {
176
- cleanup();
177
- reject(signal.reason);
178
- };
179
- signal.addEventListener("abort", onAbort, { once: true });
180
- promise.then((value) => {
181
- cleanup();
182
- resolve(value);
183
- }, (error) => {
184
- cleanup();
185
- reject(error);
186
- });
187
- });
188
- }
189
147
  async function fetchJson(url, apiKey, options) {
190
148
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
191
149
  const response = await fetch(url, {
@@ -199,169 +157,6 @@ async function fetchJson(url, apiKey, options) {
199
157
  const data = (await response.json());
200
158
  return { ok: true, data };
201
159
  }
202
- async function fetchPublicJson(url, options) {
203
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
204
- const response = await fetch(url, {
205
- headers: { Accept: "application/json" },
206
- signal: options.signal
207
- ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
208
- : AbortSignal.timeout(timeoutMs),
209
- });
210
- if (!response.ok)
211
- throw new Error(`${url} returned ${response.status}`);
212
- return (await response.json());
213
- }
214
- function isRecord(value) {
215
- return typeof value === "object" && value !== null && !Array.isArray(value);
216
- }
217
- function finiteNumber(value) {
218
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
219
- }
220
- function normalizeModelsDevCatalog(value) {
221
- if (!isRecord(value))
222
- return undefined;
223
- const catalog = {};
224
- for (const [providerId, providerValue] of Object.entries(value)) {
225
- if (!isRecord(providerValue) || !isRecord(providerValue.models))
226
- continue;
227
- const models = {};
228
- for (const [modelId, modelValue] of Object.entries(providerValue.models)) {
229
- if (!isRecord(modelValue))
230
- continue;
231
- const model = {};
232
- if (typeof modelValue.name === "string")
233
- model.name = modelValue.name;
234
- if (typeof modelValue.reasoning === "boolean")
235
- model.reasoning = modelValue.reasoning;
236
- if (isRecord(modelValue.modalities) && Array.isArray(modelValue.modalities.input)) {
237
- const input = modelValue.modalities.input.filter((entry) => typeof entry === "string");
238
- if (input.length > 0)
239
- model.modalities = { input };
240
- }
241
- if (isRecord(modelValue.limit)) {
242
- const limit = {};
243
- const context = finiteNumber(modelValue.limit.context);
244
- const input = finiteNumber(modelValue.limit.input);
245
- const output = finiteNumber(modelValue.limit.output);
246
- if (context !== undefined)
247
- limit.context = context;
248
- if (input !== undefined)
249
- limit.input = input;
250
- if (output !== undefined)
251
- limit.output = output;
252
- if (Object.keys(limit).length > 0)
253
- model.limit = limit;
254
- }
255
- if (isRecord(modelValue.cost)) {
256
- const cost = {};
257
- const input = finiteNumber(modelValue.cost.input);
258
- const output = finiteNumber(modelValue.cost.output);
259
- const cacheRead = finiteNumber(modelValue.cost.cache_read);
260
- const cacheWrite = finiteNumber(modelValue.cost.cache_write);
261
- if (input !== undefined)
262
- cost.input = input;
263
- if (output !== undefined)
264
- cost.output = output;
265
- if (cacheRead !== undefined)
266
- cost.cache_read = cacheRead;
267
- if (cacheWrite !== undefined)
268
- cost.cache_write = cacheWrite;
269
- if (Object.keys(cost).length > 0)
270
- model.cost = cost;
271
- }
272
- models[modelId] = model;
273
- }
274
- catalog[providerId] = { models };
275
- }
276
- return catalog;
277
- }
278
- async function readModelsDevCache(path) {
279
- try {
280
- const parsed = JSON.parse(await readFile(path, "utf8"));
281
- if (!isRecord(parsed) ||
282
- typeof parsed.fetchedAt !== "number" ||
283
- !Number.isFinite(parsed.fetchedAt) ||
284
- parsed.fetchedAt < 0 ||
285
- parsed.fetchedAt > Date.now()) {
286
- return undefined;
287
- }
288
- const catalog = normalizeModelsDevCatalog(parsed.catalog);
289
- return catalog ? { fetchedAt: parsed.fetchedAt, catalog } : undefined;
290
- }
291
- catch {
292
- return undefined;
293
- }
294
- }
295
- function refreshModelsDevCatalog(key, options) {
296
- const active = modelsDevRefreshes.get(key);
297
- if (active)
298
- return active;
299
- const refresh = (async () => {
300
- try {
301
- const catalog = normalizeModelsDevCatalog(await fetchPublicJson(MODELS_DEV_URL, options));
302
- if (!catalog)
303
- return undefined;
304
- const cache = { fetchedAt: Date.now(), catalog };
305
- modelsDevCaches.set(key, cache);
306
- if (options.modelsDevCachePath) {
307
- await writeJsonAtomic(options.modelsDevCachePath, cache).catch(() => undefined);
308
- }
309
- return catalog;
310
- }
311
- catch {
312
- return undefined;
313
- }
314
- finally {
315
- modelsDevRefreshes.delete(key);
316
- }
317
- })();
318
- modelsDevRefreshes.set(key, refresh);
319
- return refresh;
320
- }
321
- async function getModelsDevCatalog(options) {
322
- const key = options.modelsDevCachePath ?? MODELS_DEV_URL;
323
- const refreshOptions = { ...options, timeoutMs: DEFAULT_TIMEOUT_MS, signal: undefined };
324
- let cache = modelsDevCaches.get(key);
325
- if (!cache && options.modelsDevCachePath) {
326
- cache = await readModelsDevCache(options.modelsDevCachePath);
327
- if (cache)
328
- modelsDevCaches.set(key, cache);
329
- }
330
- if (!cache) {
331
- const timeout = AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
332
- return awaitWithSignal(refreshModelsDevCatalog(key, refreshOptions), options.signal ? AbortSignal.any([options.signal, timeout]) : timeout);
333
- }
334
- if (Date.now() - cache.fetchedAt < MODELS_DEV_CACHE_TTL_MS)
335
- return cache.catalog;
336
- void refreshModelsDevCatalog(key, refreshOptions);
337
- return cache.catalog;
338
- }
339
- function mapModelsDevMetadata(model) {
340
- if (!model)
341
- return {};
342
- const metadata = {};
343
- if (model.name)
344
- metadata.name = model.name;
345
- if (model.reasoning !== undefined)
346
- metadata.reasoning = model.reasoning;
347
- if (model.modalities?.input) {
348
- metadata.input = model.modalities.input.includes("image") ? ["text", "image"] : ["text"];
349
- }
350
- const contextWindow = model.limit?.context ?? model.limit?.input;
351
- if (contextWindow !== undefined)
352
- metadata.contextWindow = contextWindow;
353
- if (model.limit?.output !== undefined)
354
- metadata.maxTokens = model.limit.output;
355
- if (model.cost) {
356
- metadata.cost = {
357
- input: model.cost.input ?? 0,
358
- output: model.cost.output ?? 0,
359
- cacheRead: model.cost.cache_read ?? 0,
360
- cacheWrite: model.cost.cache_write ?? 0,
361
- };
362
- }
363
- return metadata;
364
- }
365
160
  function mapReasoningEfforts(info) {
366
161
  const flags = [
367
162
  ["off", "none", info.supports_none_reasoning_effort],
@@ -428,21 +223,20 @@ function mapFromHealthEndpoint(entry) {
428
223
  compat: buildCompat(id),
429
224
  };
430
225
  }
431
- function mapFromModelsList(entry, modelsDev) {
226
+ function mapFromModelsList(entry) {
432
227
  const id = entry.id;
433
228
  if (!id)
434
229
  return undefined;
435
230
  const catalogModel = findCatalogModel(id, entry.owned_by);
436
- const modelsDevMetadata = mapModelsDevMetadata(findModelsDevModel(modelsDev, id, entry.owned_by));
437
231
  return {
438
232
  id,
439
- name: modelsDevMetadata.name ?? catalogModel?.name ?? `${id} (no metadata)`,
440
- reasoning: modelsDevMetadata.reasoning ?? catalogModel?.reasoning ?? false,
233
+ name: catalogModel?.name ?? `${id} (no metadata)`,
234
+ reasoning: catalogModel?.reasoning ?? false,
441
235
  thinkingLevelMap: catalogModel?.thinkingLevelMap,
442
- input: modelsDevMetadata.input ?? catalogModel?.input ?? ["text"],
443
- cost: modelsDevMetadata.cost ?? catalogModel?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
444
- contextWindow: modelsDevMetadata.contextWindow ?? catalogModel?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
445
- maxTokens: modelsDevMetadata.maxTokens ?? catalogModel?.maxTokens ?? DEFAULT_MAX_TOKENS,
236
+ input: catalogModel?.input ?? ["text"],
237
+ cost: catalogModel?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
238
+ contextWindow: catalogModel?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
239
+ maxTokens: catalogModel?.maxTokens ?? DEFAULT_MAX_TOKENS,
446
240
  compat: buildCompat(id),
447
241
  };
448
242
  }
@@ -481,7 +275,7 @@ function deduplicateModels(models) {
481
275
  });
482
276
  }
483
277
  export async function discoverModels(baseUrl, apiKey, options = {}) {
484
- const base = normalizeBaseUrl(baseUrl);
278
+ const base = normalizeBaseUrl(baseUrl, options.allowInsecureHttp);
485
279
  const progress = options.silent ? undefined : options.onProgress;
486
280
  progress?.("Querying /model/info endpoint...");
487
281
  const infoResult = await fetchJson(`${base}/model/info`, apiKey, options);
@@ -507,15 +301,10 @@ export async function discoverModels(baseUrl, apiKey, options = {}) {
507
301
  // Ref: docs.litellm.ai/docs/proxy/model_discovery
508
302
  if (models.some((m) => m.id.includes("*"))) {
509
303
  progress?.("/model/info has wildcard entries, expanding via /v1/models...");
510
- let modelsDev;
511
- if (options.modelsDev !== false) {
512
- progress?.("Loading models.dev catalog for metadata enrichment...");
513
- modelsDev = await getModelsDevCatalog(options);
514
- }
515
304
  const listResult = await fetchJson(`${base}/v1/models`, apiKey, options);
516
305
  if (listResult.ok) {
517
306
  const expanded = (listResult.data.data ?? [])
518
- .map((entry) => mapFromModelsList(entry, modelsDev))
307
+ .map(mapFromModelsList)
519
308
  .filter((m) => m !== undefined && !m.id.includes("*"));
520
309
  const seen = new Set(models.map((m) => m.id));
521
310
  models = [...models.filter((m) => !m.id.includes("*")), ...expanded.filter((m) => !seen.has(m.id))];
@@ -537,13 +326,8 @@ export async function discoverModels(baseUrl, apiKey, options = {}) {
537
326
  }
538
327
  throw new Error(`/v1/models returned ${listResult.status}`);
539
328
  }
540
- let modelsDev;
541
- if (options.modelsDev !== false) {
542
- progress?.("Loading models.dev catalog for metadata enrichment...");
543
- modelsDev = await getModelsDevCatalog(options);
544
- }
545
329
  const models = (listResult.data.data ?? [])
546
- .map((entry) => mapFromModelsList(entry, modelsDev))
330
+ .map(mapFromModelsList)
547
331
  .filter((m) => m !== undefined);
548
332
  return { source: "models_list", models: deduplicateModels(models) };
549
333
  }
package/dist/index.js CHANGED
@@ -2,13 +2,13 @@ import { execFileSync } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { setTimeout as delay } from "node:timers/promises";
5
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+ import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
6
6
  import { setupLiteLLMCostTracking } from "./cost.js";
7
7
  import { discoverModels, isGpt55Model, isMoonshotModel, normalizeBaseUrl, shouldSuppressReasoningContent, } from "./discover.js";
8
8
  import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
9
9
  import { getSessionIdFromFile } from "./litellm.js";
10
10
  import { createMcpToolDefinitions } from "./mcp-tools.js";
11
- import { createLiteLLMProvider } from "./provider.js";
11
+ import { createLiteLLMProvider, toNativeModels } from "./provider.js";
12
12
  import { createSkillsPromptSection, createSkillToolDefinitions, listSkills } from "./skills.js";
13
13
  const PROVIDER_NAME = "litellm";
14
14
  const SETTINGS_KEY = "litellm";
@@ -20,22 +20,15 @@ const ENV_TIMEOUT = "LITELLM_DISCOVERY_TIMEOUT_MS";
20
20
  const ENV_CLI_JWT_EXPIRATION_HOURS = "LITELLM_CLI_JWT_EXPIRATION_HOURS";
21
21
  const ENV_OFFLINE = "LITELLM_OFFLINE";
22
22
  const ENV_VERBOSE_DISCOVERY = "LITELLM_VERBOSE_DISCOVERY";
23
- const ENV_MODELS_DEV = "LITELLM_MODELS_DEV";
24
23
  const DEFAULT_TIMEOUT_MS = 5000;
24
+ const SEED_TIMEOUT_MS = 3000;
25
25
  const LOGIN_TIMEOUT_MS = 10_000;
26
26
  const CLI_SSO_POLL_INTERVAL_MS = 2_000;
27
27
  const CLI_SSO_EXPIRES_IN_SECONDS = 600;
28
28
  const DEFAULT_CLI_JWT_EXPIRATION_HOURS = 24;
29
- const MODELS_DEV_CACHE_FILENAME = "litellm-models-dev.json";
30
29
  const TOKEN_REFRESH_LEAD_MS = 5 * 60 * 1000;
31
30
  const PERMANENT_TOKEN_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
32
31
  const EXPIRE_TOKEN_IMMEDIATELY = 0;
33
- function getModelsDevDiscoveryOptions() {
34
- return {
35
- modelsDev: process.env[ENV_MODELS_DEV] !== "0",
36
- modelsDevCachePath: join(getAgentDir(), MODELS_DEV_CACHE_FILENAME),
37
- };
38
- }
39
32
  function isPlainObject(value) {
40
33
  return typeof value === "object" && value !== null && !Array.isArray(value);
41
34
  }
@@ -342,7 +335,7 @@ async function resolveCredentials(definition, { executeHelpers = true } = {}) {
342
335
  apiKeyConfig = `$${ENV_API_KEY}`;
343
336
  }
344
337
  return {
345
- baseUrl: configuredBase ? normalizeBaseUrl(configuredBase) : undefined,
338
+ baseUrl: configuredBase ? normalizeBaseUrl(configuredBase, definition.allowInsecureHttp) : undefined,
346
339
  apiKey: apiKey || undefined,
347
340
  apiKeyConfig,
348
341
  };
@@ -359,6 +352,19 @@ function getDiscoveryTimeoutMs() {
359
352
  function isOffline() {
360
353
  return process.env[ENV_OFFLINE] === "1";
361
354
  }
355
+ /** Pi disables all model network access when PI_OFFLINE is set; activation must honour it too. */
356
+ function isHostOffline() {
357
+ return process.env.PI_OFFLINE !== undefined;
358
+ }
359
+ /**
360
+ * Absolute budget for activation-time discovery. Pi cannot paint its UI until every extension has
361
+ * activated, so this is deliberately capped rather than following LITELLM_DISCOVERY_TIMEOUT_MS,
362
+ * which is a per-request timeout that deployments raise to tens of seconds. Missing the budget only
363
+ * costs the startup seed; Pi's own refresh and /model still populate the catalog.
364
+ */
365
+ function getSeedTimeoutMs() {
366
+ return Math.min(getDiscoveryTimeoutMs(), SEED_TIMEOUT_MS);
367
+ }
362
368
  function isVerboseDiscovery() {
363
369
  return process.env[ENV_VERBOSE_DISCOVERY] === "1";
364
370
  }
@@ -387,6 +393,7 @@ function getProviderDefinitions(settings) {
387
393
  useDefaultEnv: isDefault,
388
394
  useGcloudTokenAuth: isDefault,
389
395
  enableOAuth: isDefault,
396
+ allowInsecureHttp: raw?.allowInsecureHttp === true,
390
397
  });
391
398
  const definitions = [makeDefinition(PROVIDER_NAME, defaultSettings, true)];
392
399
  for (const [name, raw] of Object.entries(providerSettings ?? {})) {
@@ -399,7 +406,7 @@ function getProviderDefinitions(settings) {
399
406
  }
400
407
  return definitions;
401
408
  }
402
- async function loginApiKey(interaction) {
409
+ async function loginApiKey(interaction, allowInsecureHttp = false) {
403
410
  const rawBaseUrl = (await interaction.prompt({
404
411
  type: "text",
405
412
  message: "Enter LiteLLM proxy URL (no trailing /v1):",
@@ -410,7 +417,7 @@ async function loginApiKey(interaction) {
410
417
  const key = (await interaction.prompt({ type: "secret", message: "Enter API key:" })).trim();
411
418
  if (!key)
412
419
  throw new Error("Both base URL and API key are required");
413
- return { type: "api_key", key, env: { [ENV_BASE_URL]: normalizeBaseUrl(rawBaseUrl) } };
420
+ return { type: "api_key", key, env: { [ENV_BASE_URL]: normalizeBaseUrl(rawBaseUrl, allowInsecureHttp) } };
414
421
  }
415
422
  async function startCliSso(baseUrl, signal, headers) {
416
423
  const response = await fetch(`${baseUrl}/sso/cli/start`, {
@@ -553,7 +560,7 @@ async function loginWithPastedToken(interaction, baseUrl, headers) {
553
560
  }
554
561
  return { type: "oauth", access, refresh: "", expires, baseUrl };
555
562
  }
556
- async function loginOAuth(interaction, headers) {
563
+ async function loginOAuth(interaction, headers, allowInsecureHttp = false) {
557
564
  const rawBaseUrl = (await interaction.prompt({
558
565
  type: "text",
559
566
  message: "Enter LiteLLM proxy URL (no trailing /v1):",
@@ -561,7 +568,7 @@ async function loginOAuth(interaction, headers) {
561
568
  })).trim();
562
569
  if (!rawBaseUrl)
563
570
  throw new Error("Base URL is required");
564
- const baseUrl = normalizeBaseUrl(rawBaseUrl);
571
+ const baseUrl = normalizeBaseUrl(rawBaseUrl, allowInsecureHttp);
565
572
  const cliSso = await startCliSso(baseUrl, interaction.signal, headers);
566
573
  if (!cliSso)
567
574
  return loginWithPastedToken(interaction, baseUrl, headers);
@@ -604,7 +611,7 @@ async function resolveApiKeyAuth(definition, ctx, credential, executeHelpers = t
604
611
  if (stored) {
605
612
  source = "stored credential";
606
613
  creds = {
607
- baseUrl: baseUrl ? normalizeBaseUrl(baseUrl) : undefined,
614
+ baseUrl: baseUrl ? normalizeBaseUrl(baseUrl, definition.allowInsecureHttp) : undefined,
608
615
  apiKey: stored,
609
616
  };
610
617
  }
@@ -641,7 +648,7 @@ async function resolveApiKeyAuth(definition, ctx, credential, executeHelpers = t
641
648
  }
642
649
  }
643
650
  if (!creds.baseUrl && baseUrl)
644
- creds.baseUrl = normalizeBaseUrl(baseUrl);
651
+ creds.baseUrl = normalizeBaseUrl(baseUrl, definition.allowInsecureHttp);
645
652
  }
646
653
  if (!creds.apiKey)
647
654
  return undefined;
@@ -651,7 +658,7 @@ async function resolveApiKeyAuth(definition, ctx, credential, executeHelpers = t
651
658
  baseUrl: creds.baseUrl ? `${creds.baseUrl}/v1` : undefined,
652
659
  headers: await resolveHeadersFromContext(definition, ctx.env),
653
660
  },
654
- env: baseUrl ? { [ENV_BASE_URL]: normalizeBaseUrl(baseUrl) } : undefined,
661
+ env: baseUrl ? { [ENV_BASE_URL]: normalizeBaseUrl(baseUrl, definition.allowInsecureHttp) } : undefined,
655
662
  source: source ?? creds.apiKeyConfig ?? ENV_API_KEY,
656
663
  };
657
664
  }
@@ -659,7 +666,9 @@ function createProviderAuth(definition) {
659
666
  return {
660
667
  apiKey: {
661
668
  name: `${definition.displayName} API key`,
662
- login: definition.name === PROVIDER_NAME ? loginApiKey : undefined,
669
+ login: definition.name === PROVIDER_NAME
670
+ ? (interaction) => loginApiKey(interaction, definition.allowInsecureHttp)
671
+ : undefined,
663
672
  check: async ({ ctx, credential }) => {
664
673
  const baseUrl = credential?.env?.[ENV_BASE_URL] ??
665
674
  definition.baseUrl ??
@@ -695,14 +704,16 @@ function createProviderAuth(definition) {
695
704
  ? {
696
705
  name: "LiteLLM SSO",
697
706
  loginLabel: "Sign in with LiteLLM SSO",
698
- login: (interaction) => loginOAuth(interaction, resolveHeaders(definition)),
707
+ login: (interaction) => loginOAuth(interaction, resolveHeaders(definition), definition.allowInsecureHttp),
699
708
  refresh: async (credential, signal) => ({
700
709
  ...(await refreshLiteLLM(credential, signal)),
701
710
  type: "oauth",
702
711
  }),
703
712
  toAuth: async (credential) => ({
704
713
  apiKey: credential.access,
705
- baseUrl: credential.baseUrl ? `${normalizeBaseUrl(String(credential.baseUrl))}/v1` : undefined,
714
+ baseUrl: credential.baseUrl
715
+ ? `${normalizeBaseUrl(String(credential.baseUrl), definition.allowInsecureHttp)}/v1`
716
+ : undefined,
706
717
  headers: resolveHeaders(definition),
707
718
  }),
708
719
  }
@@ -933,23 +944,29 @@ export default async function (pi) {
933
944
  const baseUrl = cleanConfig(definition.baseUrl) ??
934
945
  (definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined) ??
935
946
  "https://litellm.example.com";
936
- return `${normalizeBaseUrl(baseUrl)}/v1`;
947
+ return `${normalizeBaseUrl(baseUrl, definition.allowInsecureHttp)}/v1`;
937
948
  }
938
- async function authForCredential(definition, credential) {
939
- if (credential.type === "oauth") {
949
+ async function authForCredential(definition, credential, executeHelpers = true) {
950
+ if (credential?.type === "oauth") {
940
951
  const baseUrl = typeof credential.baseUrl === "string"
941
- ? normalizeBaseUrl(credential.baseUrl)
942
- : normalizeBaseUrl(requestBaseUrl(definition));
943
- return { baseUrl, apiKey: credential.access, headers: resolveHeaders(definition) };
952
+ ? normalizeBaseUrl(credential.baseUrl, definition.allowInsecureHttp)
953
+ : normalizeBaseUrl(requestBaseUrl(definition), definition.allowInsecureHttp);
954
+ return {
955
+ baseUrl,
956
+ apiKey: credential.access,
957
+ headers: resolveHeaders(definition),
958
+ allowInsecureHttp: definition.allowInsecureHttp,
959
+ };
944
960
  }
945
- const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential);
961
+ const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential, executeHelpers);
946
962
  if (!resolved?.auth.apiKey) {
947
963
  throw new Error(`no credentials for ${definition.name}. Run /login litellm or set env vars.`);
948
964
  }
949
965
  return {
950
- baseUrl: normalizeBaseUrl(resolved.auth.baseUrl ?? requestBaseUrl(definition)),
966
+ baseUrl: normalizeBaseUrl(resolved.auth.baseUrl ?? requestBaseUrl(definition), definition.allowInsecureHttp),
951
967
  apiKey: resolved.auth.apiKey,
952
968
  headers: resolved.auth.headers,
969
+ allowInsecureHttp: definition.allowInsecureHttp,
953
970
  };
954
971
  }
955
972
  let registeredMcpIdentity;
@@ -957,7 +974,7 @@ export default async function (pi) {
957
974
  let defaultRuntimeAuth;
958
975
  function mcpCatalogIdentity(auth) {
959
976
  return JSON.stringify({
960
- baseUrl: normalizeBaseUrl(auth.baseUrl),
977
+ baseUrl: normalizeBaseUrl(auth.baseUrl, auth.allowInsecureHttp),
961
978
  apiKey: auth.apiKey,
962
979
  headers: Object.entries(auth.headers ?? {}).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
963
980
  });
@@ -973,9 +990,10 @@ export default async function (pi) {
973
990
  return undefined;
974
991
  const headers = Object.fromEntries(Object.entries(auth.auth.headers ?? provider?.headers ?? {}).filter((entry) => typeof entry[1] === "string"));
975
992
  return {
976
- baseUrl: normalizeBaseUrl(baseUrl),
993
+ baseUrl: normalizeBaseUrl(baseUrl, definitions[0]?.allowInsecureHttp),
977
994
  apiKey,
978
995
  headers: Object.keys(headers).length > 0 ? headers : undefined,
996
+ allowInsecureHttp: definitions[0]?.allowInsecureHttp,
979
997
  };
980
998
  }
981
999
  async function resolveDefaultRuntimeAuth(ctx) {
@@ -1045,27 +1063,53 @@ export default async function (pi) {
1045
1063
  mcpRegistration = undefined;
1046
1064
  }
1047
1065
  }
1048
- for (const definition of definitions) {
1066
+ async function runDiscovery(auth, signal) {
1067
+ const result = await discoverModels(auth.baseUrl, auth.apiKey, {
1068
+ timeoutMs: getDiscoveryTimeoutMs(),
1069
+ signal,
1070
+ headers: auth.headers,
1071
+ allowInsecureHttp: auth.allowInsecureHttp,
1072
+ silent: !isVerboseDiscovery(),
1073
+ onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
1074
+ });
1075
+ signal?.throwIfAborted();
1076
+ return { ...result, baseUrl: `${normalizeBaseUrl(auth.baseUrl, auth.allowInsecureHttp)}/v1` };
1077
+ }
1078
+ // Pi's startup path only ever refreshes with allowNetwork:false, and it returns before the
1079
+ // network phase, so the provider would stay empty until the user opens /model. Discover here
1080
+ // instead, the way 1.x did, and hand the catalog over as the provider's baseline models.
1081
+ async function seedModels(definition) {
1082
+ if (discoveryDisabledReason() || isHostOffline())
1083
+ return [];
1084
+ try {
1085
+ const stored = readStoredCredential(definition.name, join(getAgentDir(), "auth.json"));
1086
+ // executeHelpers:false — activation must never run the user's key helper as a side effect,
1087
+ // so helper-backed setups keep waiting for Pi's own refresh.
1088
+ const auth = await authForCredential(definition, stored, false);
1089
+ const result = await runDiscovery(auth, AbortSignal.timeout(getSeedTimeoutMs()));
1090
+ return toNativeModels(definition.name, result.baseUrl, result.models);
1091
+ }
1092
+ catch (error) {
1093
+ // No credentials yet is the normal unconfigured case; anything else is worth one line.
1094
+ if (isVerboseDiscovery()) {
1095
+ process.stderr.write(`LiteLLM (${definition.name}): startup discovery skipped (${error instanceof Error ? error.message : String(error)}).\n`);
1096
+ }
1097
+ return [];
1098
+ }
1099
+ }
1100
+ const seeded = await Promise.all(definitions.map(seedModels));
1101
+ for (const [index, definition] of definitions.entries()) {
1049
1102
  const provider = createLiteLLMProvider({
1050
1103
  id: definition.name,
1051
1104
  name: definition.displayName,
1052
1105
  baseUrl: requestBaseUrl(definition),
1053
1106
  auth: createProviderAuth(definition),
1107
+ models: seeded[index],
1054
1108
  discover: async (credential, signal) => {
1055
1109
  const disabledReason = discoveryDisabledReason();
1056
1110
  if (disabledReason)
1057
1111
  throw new Error(`discovery disabled (${disabledReason})`);
1058
- const auth = await authForCredential(definition, credential);
1059
- const result = await discoverModels(auth.baseUrl, auth.apiKey, {
1060
- ...getModelsDevDiscoveryOptions(),
1061
- timeoutMs: getDiscoveryTimeoutMs(),
1062
- signal,
1063
- headers: auth.headers,
1064
- silent: !isVerboseDiscovery(),
1065
- onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
1066
- });
1067
- signal?.throwIfAborted();
1068
- return { ...result, baseUrl: `${normalizeBaseUrl(auth.baseUrl)}/v1` };
1112
+ return runDiscovery(await authForCredential(definition, credential), signal);
1069
1113
  },
1070
1114
  });
1071
1115
  Object.assign(provider, { headers: resolveHeaders(definition) });
@@ -1104,16 +1148,27 @@ export default async function (pi) {
1104
1148
  return;
1105
1149
  return prepareLiteLLMRequestPayload(event.payload, ctx.model?.id, ctx.model?.api, sessionId);
1106
1150
  });
1151
+ // Skills enrichment is best-effort: an expired credential or an unreachable proxy must not
1152
+ // report an extension error on every turn. The failing model request states the real problem.
1107
1153
  pi.on("before_agent_start", async (event, ctx) => {
1108
1154
  defaultRuntimeAuth = undefined;
1109
1155
  if (!skillsEnabled || discoveryDisabledReason())
1110
1156
  return;
1111
- const auth = await getRuntimeAuth(ctx);
1112
- if (!auth)
1157
+ let section;
1158
+ try {
1159
+ const auth = await getRuntimeAuth(ctx);
1160
+ if (!auth)
1161
+ return;
1162
+ defaultRuntimeAuth = auth;
1163
+ const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers, auth.allowInsecureHttp);
1164
+ section = createSkillsPromptSection(skills);
1165
+ }
1166
+ catch (error) {
1167
+ if (isVerboseDiscovery()) {
1168
+ process.stderr.write(`LiteLLM: skipping Skills (${error instanceof Error ? error.message : String(error)}).\n`);
1169
+ }
1113
1170
  return;
1114
- defaultRuntimeAuth = auth;
1115
- const skills = await listSkills(defaultRuntimeAuth.baseUrl, defaultRuntimeAuth.apiKey, defaultRuntimeAuth.headers);
1116
- const section = createSkillsPromptSection(skills);
1171
+ }
1117
1172
  if (!section)
1118
1173
  return;
1119
1174
  return { systemPrompt: `${event.systemPrompt}\n\n${section}` };
@@ -1,5 +1,5 @@
1
1
  import { type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import type { LiteLLMMcpTool, LiteLLMRuntimeAuth } from "./types.js";
3
- export declare function discoverMcpTools(baseUrl: string, apiKey: string, headers?: Record<string, string>, onProgress?: (message: string) => void, parentSignal?: AbortSignal): Promise<LiteLLMMcpTool[]>;
4
- export declare function executeMcpTool(baseUrl: string, apiKey: string, serverId: string, toolName: string, args: Record<string, unknown>, headers?: Record<string, string>): Promise<string>;
3
+ export declare function discoverMcpTools(baseUrl: string, apiKey: string, headers?: Record<string, string>, onProgress?: (message: string) => void, parentSignal?: AbortSignal, allowInsecureHttp?: boolean): Promise<LiteLLMMcpTool[]>;
4
+ export declare function executeMcpTool(baseUrl: string, apiKey: string, serverId: string, toolName: string, args: Record<string, unknown>, headers?: Record<string, string>, allowInsecureHttp?: boolean): Promise<string>;
5
5
  export declare function createMcpToolDefinitions(getAuth: (ctx?: ExtensionContext) => Promise<LiteLLMRuntimeAuth>, onProgress?: (message: string) => void, signal?: AbortSignal): Promise<ToolDefinition[]>;
package/dist/mcp-tools.js CHANGED
@@ -49,11 +49,11 @@ function normalizeMcpTool(value) {
49
49
  input_schema: inputSchema,
50
50
  };
51
51
  }
52
- export async function discoverMcpTools(baseUrl, apiKey, headers, onProgress, parentSignal) {
52
+ export async function discoverMcpTools(baseUrl, apiKey, headers, onProgress, parentSignal, allowInsecureHttp = false) {
53
53
  onProgress?.("Discovering MCP tools from server...");
54
54
  try {
55
55
  onProgress?.("Querying MCP tools/list endpoint...");
56
- const response = await fetch(`${normalizeBaseUrl(baseUrl)}/mcp-rest/tools/list`, {
56
+ const response = await fetch(`${normalizeBaseUrl(baseUrl, allowInsecureHttp)}/mcp-rest/tools/list`, {
57
57
  headers: {
58
58
  ...headers,
59
59
  Authorization: `Bearer ${apiKey}`,
@@ -80,9 +80,9 @@ export async function discoverMcpTools(baseUrl, apiKey, headers, onProgress, par
80
80
  throw error;
81
81
  }
82
82
  }
83
- export async function executeMcpTool(baseUrl, apiKey, serverId, toolName, args, headers) {
83
+ export async function executeMcpTool(baseUrl, apiKey, serverId, toolName, args, headers, allowInsecureHttp = false) {
84
84
  for (let attempt = 0; attempt <= MCP_RETRY_ATTEMPTS; attempt++) {
85
- const result = await executeMcpToolOnce(baseUrl, apiKey, serverId, toolName, args, headers);
85
+ const result = await executeMcpToolOnce(baseUrl, apiKey, serverId, toolName, args, headers, allowInsecureHttp);
86
86
  if (attempt < MCP_RETRY_ATTEMPTS && result.retryable) {
87
87
  await sleep(MCP_RETRY_DELAY_MS);
88
88
  continue;
@@ -91,11 +91,11 @@ export async function executeMcpTool(baseUrl, apiKey, serverId, toolName, args,
91
91
  }
92
92
  return `Error calling ${toolName} on ${serverId}: retry attempts exhausted`;
93
93
  }
94
- async function executeMcpToolOnce(baseUrl, apiKey, serverId, toolName, args, headers) {
94
+ async function executeMcpToolOnce(baseUrl, apiKey, serverId, toolName, args, headers, allowInsecureHttp = false) {
95
95
  try {
96
96
  let response;
97
97
  try {
98
- response = await fetch(`${normalizeBaseUrl(baseUrl)}/mcp-rest/tools/call`, {
98
+ response = await fetch(`${normalizeBaseUrl(baseUrl, allowInsecureHttp)}/mcp-rest/tools/call`, {
99
99
  method: "POST",
100
100
  headers: {
101
101
  ...headers,
@@ -152,7 +152,7 @@ function buildParameters(inputSchema) {
152
152
  }
153
153
  export async function createMcpToolDefinitions(getAuth, onProgress, signal) {
154
154
  const discoveryAuth = await getAuth();
155
- const tools = await discoverMcpTools(discoveryAuth.baseUrl, discoveryAuth.apiKey, discoveryAuth.headers, onProgress, signal);
155
+ const tools = await discoverMcpTools(discoveryAuth.baseUrl, discoveryAuth.apiKey, discoveryAuth.headers, onProgress, signal, discoveryAuth.allowInsecureHttp);
156
156
  return tools.map((mcpTool) => {
157
157
  const safeServer = sanitizeName(mcpTool.server_name);
158
158
  const safeTool = sanitizeName(mcpTool.name);
@@ -170,7 +170,7 @@ export async function createMcpToolDefinitions(getAuth, onProgress, signal) {
170
170
  const args = Object.keys(rawParams).length === 1 && rawParams.args && typeof rawParams.args === "object"
171
171
  ? rawParams.args
172
172
  : rawParams;
173
- const text = await executeMcpTool(auth.baseUrl, auth.apiKey, mcpTool.server_id ?? mcpTool.server_name, mcpTool.name, args, auth.headers);
173
+ const text = await executeMcpTool(auth.baseUrl, auth.apiKey, mcpTool.server_id ?? mcpTool.server_name, mcpTool.name, args, auth.headers, auth.allowInsecureHttp);
174
174
  return {
175
175
  content: [{ type: "text", text }],
176
176
  details: { server: mcpTool.server_name, serverId: mcpTool.server_id, tool: mcpTool.name },
@@ -5,6 +5,9 @@ export type LiteLLMProviderOptions = {
5
5
  name: string;
6
6
  baseUrl: string;
7
7
  auth: ProviderAuth;
8
+ /** Catalog discovered during activation. Pi's startup refresh never allows network access, so
9
+ * without a seed the provider stays empty until the user opens /model. */
10
+ models?: readonly Model<LiteLLMApi>[];
8
11
  discover(credential: Credential, signal?: AbortSignal): Promise<DiscoveryResult & {
9
12
  baseUrl?: string;
10
13
  }>;
package/dist/provider.js CHANGED
@@ -15,7 +15,7 @@ export function createLiteLLMProvider(options) {
15
15
  name: options.name,
16
16
  baseUrl: options.baseUrl,
17
17
  auth: options.auth,
18
- models: [],
18
+ models: options.models ?? [],
19
19
  async fetchModels(context) {
20
20
  if (!context.credential)
21
21
  throw new Error("LiteLLM model discovery requires a credential");
package/dist/skills.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import type { LiteLLMRuntimeAuth, LiteLLMSkill } from "./types.js";
3
- export declare function listSkills(baseUrl: string, apiKey: string, headers?: Record<string, string>): Promise<LiteLLMSkill[]>;
3
+ export declare function listSkills(baseUrl: string, apiKey: string, headers?: Record<string, string>, allowInsecureHttp?: boolean): Promise<LiteLLMSkill[]>;
4
4
  export declare function createSkill(baseUrl: string, apiKey: string, input: {
5
5
  name: string;
6
6
  description?: string;
7
7
  source?: Record<string, unknown>;
8
8
  code?: string;
9
9
  inputSchema?: Record<string, unknown>;
10
- }, headers?: Record<string, string>): Promise<unknown>;
11
- export declare function deleteSkill(baseUrl: string, apiKey: string, skillId: string, headers?: Record<string, string>): Promise<void>;
10
+ }, headers?: Record<string, string>, allowInsecureHttp?: boolean): Promise<unknown>;
11
+ export declare function deleteSkill(baseUrl: string, apiKey: string, skillId: string, headers?: Record<string, string>, allowInsecureHttp?: boolean): Promise<void>;
12
12
  export declare function createSkillsPromptSection(skills: LiteLLMSkill[]): string | undefined;
13
13
  export declare function createSkillToolDefinitions(getAuth: (ctx?: ExtensionContext) => Promise<LiteLLMRuntimeAuth>): ToolDefinition[];
14
14
  export declare function resetSkillsCache(): void;
package/dist/skills.js CHANGED
@@ -17,8 +17,8 @@ function getSkillsFromBody(body) {
17
17
  }
18
18
  return [];
19
19
  }
20
- export async function listSkills(baseUrl, apiKey, headers) {
21
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
20
+ export async function listSkills(baseUrl, apiKey, headers, allowInsecureHttp = false) {
21
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
22
22
  if (skillsCache &&
23
23
  skillsCache.baseUrl === normalizedBaseUrl &&
24
24
  skillsCache.apiKey === apiKey &&
@@ -46,10 +46,10 @@ export async function listSkills(baseUrl, apiKey, headers) {
46
46
  skillsCache = { baseUrl: normalizedBaseUrl, apiKey, fetchedAt: Date.now(), skills };
47
47
  return skills;
48
48
  }
49
- export async function createSkill(baseUrl, apiKey, input, headers) {
49
+ export async function createSkill(baseUrl, apiKey, input, headers, allowInsecureHttp = false) {
50
50
  if (!input.source && !input.code)
51
51
  throw new Error("code is required when source is omitted");
52
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
52
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
53
53
  let usedSkillHub = input.source !== undefined;
54
54
  const skillHubPayload = input.source
55
55
  ? {
@@ -105,8 +105,8 @@ export async function createSkill(baseUrl, apiKey, input, headers) {
105
105
  }
106
106
  return response.json().catch(() => ({}));
107
107
  }
108
- export async function deleteSkill(baseUrl, apiKey, skillId, headers) {
109
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
108
+ export async function deleteSkill(baseUrl, apiKey, skillId, headers, allowInsecureHttp = false) {
109
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl, allowInsecureHttp);
110
110
  let skillHubError;
111
111
  const skillHubResponse = await fetch(`${normalizedBaseUrl}/claude-code/plugins/${encodeURIComponent(skillId)}`, {
112
112
  method: "DELETE",
@@ -182,7 +182,7 @@ export function createSkillToolDefinitions(getAuth) {
182
182
  parameters: Type.Object({}),
183
183
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
184
184
  const auth = await getAuth(ctx);
185
- const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers);
185
+ const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers, auth.allowInsecureHttp);
186
186
  return { content: [{ type: "text", text: formatSkills(skills) }], details: { count: skills.length } };
187
187
  },
188
188
  }),
@@ -203,7 +203,7 @@ export function createSkillToolDefinitions(getAuth) {
203
203
  source,
204
204
  code: params.code,
205
205
  inputSchema,
206
- }, auth.headers);
206
+ }, auth.headers, auth.allowInsecureHttp);
207
207
  return { content: [{ type: "text", text: "LiteLLM skill created." }], details: { result } };
208
208
  },
209
209
  }),
@@ -214,7 +214,7 @@ export function createSkillToolDefinitions(getAuth) {
214
214
  parameters: DeleteSkillParams,
215
215
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
216
216
  const auth = await getAuth(ctx);
217
- await deleteSkill(auth.baseUrl, auth.apiKey, params.skillId, auth.headers);
217
+ await deleteSkill(auth.baseUrl, auth.apiKey, params.skillId, auth.headers, auth.allowInsecureHttp);
218
218
  return { content: [{ type: "text", text: `LiteLLM skill deleted: ${params.skillId}` }], details: {} };
219
219
  },
220
220
  }),
package/dist/types.d.ts CHANGED
@@ -5,6 +5,7 @@ export type LiteLLMRuntimeAuth = {
5
5
  baseUrl: string;
6
6
  apiKey: string;
7
7
  headers?: Record<string, string>;
8
+ allowInsecureHttp?: boolean;
8
9
  };
9
10
  export type DiscoveredModel = Omit<Model<"openai-completions">, "provider" | "api" | "baseUrl"> & {
10
11
  api?: LiteLLMApi;
@@ -17,8 +18,7 @@ export interface DiscoveryOptions {
17
18
  timeoutMs?: number;
18
19
  signal?: AbortSignal;
19
20
  headers?: Record<string, string>;
20
- modelsDev?: boolean;
21
- modelsDevCachePath?: string;
21
+ allowInsecureHttp?: boolean;
22
22
  }
23
23
  export interface ModelInfoEntry {
24
24
  model_name?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-litellm",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "LiteLLM proxy provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/dist/cache.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function writeJsonAtomic(path: string, value: unknown): Promise<void>;
package/dist/cache.js DELETED
@@ -1,8 +0,0 @@
1
- import { mkdir, rename, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- export async function writeJsonAtomic(path, value) {
4
- await mkdir(dirname(path), { recursive: true });
5
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
6
- await writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
7
- await rename(tmp, path);
8
- }