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 +1 -3
- package/dist/discover.d.ts +1 -1
- package/dist/discover.js +12 -228
- package/dist/index.js +103 -48
- package/dist/mcp-tools.d.ts +2 -2
- package/dist/mcp-tools.js +8 -8
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +1 -1
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +9 -9
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +0 -8
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:
|
package/dist/discover.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
440
|
-
reasoning:
|
|
233
|
+
name: catalogModel?.name ?? `${id} (no metadata)`,
|
|
234
|
+
reasoning: catalogModel?.reasoning ?? false,
|
|
441
235
|
thinkingLevelMap: catalogModel?.thinkingLevelMap,
|
|
442
|
-
input:
|
|
443
|
-
cost:
|
|
444
|
-
contextWindow:
|
|
445
|
-
maxTokens:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
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}` };
|
package/dist/mcp-tools.d.ts
CHANGED
|
@@ -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
|
|
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 },
|
package/dist/provider.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
11
|
-
export declare function deleteSkill(baseUrl: string, apiKey: string, skillId: string, headers?: Record<string, string
|
|
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
|
-
|
|
21
|
-
modelsDevCachePath?: string;
|
|
21
|
+
allowInsecureHttp?: boolean;
|
|
22
22
|
}
|
|
23
23
|
export interface ModelInfoEntry {
|
|
24
24
|
model_name?: string;
|
package/package.json
CHANGED
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
|
-
}
|