pi-provider-litellm 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -3
- package/dist/discover.js +9 -225
- package/dist/index.js +72 -30
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +1 -1
- package/dist/types.d.ts +0 -2
- package/package.json +1 -1
- package/dist/cache.d.ts +0 -1
- package/dist/cache.js +0 -8
package/README.md
CHANGED
|
@@ -157,14 +157,11 @@ Treat the configured LiteLLM proxy as trusted: Skills can add instructions to th
|
|
|
157
157
|
| `LITELLM_DISCOVERY_TIMEOUT_MS` | `5000` | Background and explicit discovery fetch timeout in ms; `0` disables automatic discovery |
|
|
158
158
|
| `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
159
|
| `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
160
|
|
|
162
161
|
Only use a trusted `LITELLM_API_KEY_HELPER` or `!command`. Prefer an absolute executable path, keep secrets out of command arguments, and print only the token to stdout without logging it to stderr.
|
|
163
162
|
|
|
164
163
|
`LITELLM_DISCOVERY_TIMEOUT_MS=0` disables automatic and explicit refresh model discovery. It does not replace the base URL or API key settings required to send requests when you are not using `/login litellm`.
|
|
165
164
|
|
|
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
165
|
### Google ADC token auth
|
|
169
166
|
|
|
170
167
|
When your LiteLLM proxy accepts Google OAuth access tokens, you can let the extension refresh tokens from Application Default Credentials:
|
package/dist/discover.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
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
7
|
export function normalizeBaseUrl(input) {
|
|
14
8
|
const url = new URL(input);
|
|
15
9
|
const hostname = url.hostname.toLowerCase();
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -935,14 +941,14 @@ export default async function (pi) {
|
|
|
935
941
|
"https://litellm.example.com";
|
|
936
942
|
return `${normalizeBaseUrl(baseUrl)}/v1`;
|
|
937
943
|
}
|
|
938
|
-
async function authForCredential(definition, credential) {
|
|
939
|
-
if (credential
|
|
944
|
+
async function authForCredential(definition, credential, executeHelpers = true) {
|
|
945
|
+
if (credential?.type === "oauth") {
|
|
940
946
|
const baseUrl = typeof credential.baseUrl === "string"
|
|
941
947
|
? normalizeBaseUrl(credential.baseUrl)
|
|
942
948
|
: normalizeBaseUrl(requestBaseUrl(definition));
|
|
943
949
|
return { baseUrl, apiKey: credential.access, headers: resolveHeaders(definition) };
|
|
944
950
|
}
|
|
945
|
-
const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential);
|
|
951
|
+
const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential, executeHelpers);
|
|
946
952
|
if (!resolved?.auth.apiKey) {
|
|
947
953
|
throw new Error(`no credentials for ${definition.name}. Run /login litellm or set env vars.`);
|
|
948
954
|
}
|
|
@@ -1045,27 +1051,52 @@ export default async function (pi) {
|
|
|
1045
1051
|
mcpRegistration = undefined;
|
|
1046
1052
|
}
|
|
1047
1053
|
}
|
|
1048
|
-
|
|
1054
|
+
async function runDiscovery(auth, signal) {
|
|
1055
|
+
const result = await discoverModels(auth.baseUrl, auth.apiKey, {
|
|
1056
|
+
timeoutMs: getDiscoveryTimeoutMs(),
|
|
1057
|
+
signal,
|
|
1058
|
+
headers: auth.headers,
|
|
1059
|
+
silent: !isVerboseDiscovery(),
|
|
1060
|
+
onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
|
|
1061
|
+
});
|
|
1062
|
+
signal?.throwIfAborted();
|
|
1063
|
+
return { ...result, baseUrl: `${normalizeBaseUrl(auth.baseUrl)}/v1` };
|
|
1064
|
+
}
|
|
1065
|
+
// Pi's startup path only ever refreshes with allowNetwork:false, and it returns before the
|
|
1066
|
+
// network phase, so the provider would stay empty until the user opens /model. Discover here
|
|
1067
|
+
// instead, the way 1.x did, and hand the catalog over as the provider's baseline models.
|
|
1068
|
+
async function seedModels(definition) {
|
|
1069
|
+
if (discoveryDisabledReason() || isHostOffline())
|
|
1070
|
+
return [];
|
|
1071
|
+
try {
|
|
1072
|
+
const stored = readStoredCredential(definition.name, join(getAgentDir(), "auth.json"));
|
|
1073
|
+
// executeHelpers:false — activation must never run the user's key helper as a side effect,
|
|
1074
|
+
// so helper-backed setups keep waiting for Pi's own refresh.
|
|
1075
|
+
const auth = await authForCredential(definition, stored, false);
|
|
1076
|
+
const result = await runDiscovery(auth, AbortSignal.timeout(getSeedTimeoutMs()));
|
|
1077
|
+
return toNativeModels(definition.name, result.baseUrl, result.models);
|
|
1078
|
+
}
|
|
1079
|
+
catch (error) {
|
|
1080
|
+
// No credentials yet is the normal unconfigured case; anything else is worth one line.
|
|
1081
|
+
if (isVerboseDiscovery()) {
|
|
1082
|
+
process.stderr.write(`LiteLLM (${definition.name}): startup discovery skipped (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
1083
|
+
}
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const seeded = await Promise.all(definitions.map(seedModels));
|
|
1088
|
+
for (const [index, definition] of definitions.entries()) {
|
|
1049
1089
|
const provider = createLiteLLMProvider({
|
|
1050
1090
|
id: definition.name,
|
|
1051
1091
|
name: definition.displayName,
|
|
1052
1092
|
baseUrl: requestBaseUrl(definition),
|
|
1053
1093
|
auth: createProviderAuth(definition),
|
|
1094
|
+
models: seeded[index],
|
|
1054
1095
|
discover: async (credential, signal) => {
|
|
1055
1096
|
const disabledReason = discoveryDisabledReason();
|
|
1056
1097
|
if (disabledReason)
|
|
1057
1098
|
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` };
|
|
1099
|
+
return runDiscovery(await authForCredential(definition, credential), signal);
|
|
1069
1100
|
},
|
|
1070
1101
|
});
|
|
1071
1102
|
Object.assign(provider, { headers: resolveHeaders(definition) });
|
|
@@ -1104,16 +1135,27 @@ export default async function (pi) {
|
|
|
1104
1135
|
return;
|
|
1105
1136
|
return prepareLiteLLMRequestPayload(event.payload, ctx.model?.id, ctx.model?.api, sessionId);
|
|
1106
1137
|
});
|
|
1138
|
+
// Skills enrichment is best-effort: an expired credential or an unreachable proxy must not
|
|
1139
|
+
// report an extension error on every turn. The failing model request states the real problem.
|
|
1107
1140
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1108
1141
|
defaultRuntimeAuth = undefined;
|
|
1109
1142
|
if (!skillsEnabled || discoveryDisabledReason())
|
|
1110
1143
|
return;
|
|
1111
|
-
|
|
1112
|
-
|
|
1144
|
+
let section;
|
|
1145
|
+
try {
|
|
1146
|
+
const auth = await getRuntimeAuth(ctx);
|
|
1147
|
+
if (!auth)
|
|
1148
|
+
return;
|
|
1149
|
+
defaultRuntimeAuth = auth;
|
|
1150
|
+
const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers);
|
|
1151
|
+
section = createSkillsPromptSection(skills);
|
|
1152
|
+
}
|
|
1153
|
+
catch (error) {
|
|
1154
|
+
if (isVerboseDiscovery()) {
|
|
1155
|
+
process.stderr.write(`LiteLLM: skipping Skills (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
1156
|
+
}
|
|
1113
1157
|
return;
|
|
1114
|
-
|
|
1115
|
-
const skills = await listSkills(defaultRuntimeAuth.baseUrl, defaultRuntimeAuth.apiKey, defaultRuntimeAuth.headers);
|
|
1116
|
-
const section = createSkillsPromptSection(skills);
|
|
1158
|
+
}
|
|
1117
1159
|
if (!section)
|
|
1118
1160
|
return;
|
|
1119
1161
|
return { systemPrompt: `${event.systemPrompt}\n\n${section}` };
|
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/types.d.ts
CHANGED
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
|
-
}
|