pi-provider-litellm 2.3.0 → 3.0.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 +148 -24
- package/package.json +13 -18
- package/src/backend-identity.ts +100 -0
- package/src/cost.ts +52 -0
- package/src/discover.ts +1318 -0
- package/src/gcloud-token.ts +160 -0
- package/src/index.ts +2005 -0
- package/src/mcp-tools.ts +1341 -0
- package/src/model-groups.ts +947 -0
- package/src/protocols.ts +42 -0
- package/src/provider.ts +248 -0
- package/src/public-catalog.ts +272 -0
- package/src/skills.ts +275 -0
- package/src/thinking-levels.ts +35 -0
- package/src/types.ts +162 -0
- package/dist/cost.d.ts +0 -2
- package/dist/cost.js +0 -49
- package/dist/discover.d.ts +0 -12
- package/dist/discover.js +0 -380
- package/dist/gcloud-token-cli.d.ts +0 -1
- package/dist/gcloud-token-cli.js +0 -8
- package/dist/gcloud-token.d.ts +0 -7
- package/dist/gcloud-token.js +0 -133
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1226
- package/dist/litellm.d.ts +0 -1
- package/dist/litellm.js +0 -10
- package/dist/mcp-tools.d.ts +0 -5
- package/dist/mcp-tools.js +0 -181
- package/dist/provider.d.ts +0 -16
- package/dist/provider.js +0 -40
- package/dist/skills.d.ts +0 -14
- package/dist/skills.js +0 -225
- package/dist/types.d.ts +0 -108
- package/dist/types.js +0 -1
package/dist/index.js
DELETED
|
@@ -1,1226 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import { getAgentDir, readStoredCredential } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { setupLiteLLMCostTracking } from "./cost.js";
|
|
7
|
-
import { discoverModels, emitsThinkTags, isGpt55Model, isMoonshotModel, normalizeBaseUrl } from "./discover.js";
|
|
8
|
-
import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
|
|
9
|
-
import { getSessionIdFromFile } from "./litellm.js";
|
|
10
|
-
import { createMcpToolDefinitions } from "./mcp-tools.js";
|
|
11
|
-
import { createLiteLLMProvider, toNativeModels } from "./provider.js";
|
|
12
|
-
import { createSkillsPromptSection, createSkillToolDefinitions, listSkills } from "./skills.js";
|
|
13
|
-
const PROVIDER_NAME = "litellm";
|
|
14
|
-
const SETTINGS_KEY = "litellm";
|
|
15
|
-
const ENV_BASE_URL = "LITELLM_BASE_URL";
|
|
16
|
-
const ENV_API_KEY = "LITELLM_API_KEY";
|
|
17
|
-
const ENV_API_KEY_HELPER = "LITELLM_API_KEY_HELPER";
|
|
18
|
-
const ENV_HEADERS = "LITELLM_HEADERS";
|
|
19
|
-
const ENV_TIMEOUT = "LITELLM_DISCOVERY_TIMEOUT_MS";
|
|
20
|
-
const ENV_CLI_JWT_EXPIRATION_HOURS = "LITELLM_CLI_JWT_EXPIRATION_HOURS";
|
|
21
|
-
const ENV_OFFLINE = "LITELLM_OFFLINE";
|
|
22
|
-
const ENV_VERBOSE_DISCOVERY = "LITELLM_VERBOSE_DISCOVERY";
|
|
23
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
24
|
-
const SEED_TIMEOUT_MS = 3000;
|
|
25
|
-
const LOGIN_TIMEOUT_MS = 10_000;
|
|
26
|
-
const CLI_SSO_POLL_INTERVAL_MS = 2_000;
|
|
27
|
-
const CLI_SSO_EXPIRES_IN_SECONDS = 600;
|
|
28
|
-
const DEFAULT_CLI_JWT_EXPIRATION_HOURS = 24;
|
|
29
|
-
const TOKEN_REFRESH_LEAD_MS = 5 * 60 * 1000;
|
|
30
|
-
const PERMANENT_TOKEN_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
|
|
31
|
-
const EXPIRE_TOKEN_IMMEDIATELY = 0;
|
|
32
|
-
function isPlainObject(value) {
|
|
33
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
-
}
|
|
35
|
-
async function readGlobalLiteLLMSettings() {
|
|
36
|
-
try {
|
|
37
|
-
const raw = await readFile(join(getAgentDir(), "settings.json"), "utf8");
|
|
38
|
-
const parsed = JSON.parse(raw);
|
|
39
|
-
const settings = parsed[SETTINGS_KEY];
|
|
40
|
-
return settings && typeof settings === "object" && !Array.isArray(settings)
|
|
41
|
-
? settings
|
|
42
|
-
: undefined;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function cleanConfig(raw) {
|
|
49
|
-
const trimmed = raw?.trim();
|
|
50
|
-
return trimmed && trimmed !== "undefined" ? trimmed : undefined;
|
|
51
|
-
}
|
|
52
|
-
function stringSetting(value) {
|
|
53
|
-
return typeof value === "string" ? cleanConfig(value) : undefined;
|
|
54
|
-
}
|
|
55
|
-
function normalizeCommand(raw) {
|
|
56
|
-
const trimmed = cleanConfig(raw);
|
|
57
|
-
if (!trimmed)
|
|
58
|
-
return undefined;
|
|
59
|
-
return trimmed.startsWith("!") ? trimmed : `!${trimmed}`;
|
|
60
|
-
}
|
|
61
|
-
function getApiKeyHelperCommand() {
|
|
62
|
-
return normalizeCommand(process.env[ENV_API_KEY_HELPER]);
|
|
63
|
-
}
|
|
64
|
-
function parseApiKeyCommand(commandConfig) {
|
|
65
|
-
const command = commandConfig.startsWith("!") ? commandConfig.slice(1) : commandConfig;
|
|
66
|
-
if (/[\n\r;&|<>`$(){}*?~%^!]|\[|\]/.test(command))
|
|
67
|
-
throw new Error("LiteLLM API key helper shell syntax is not supported");
|
|
68
|
-
const parts = [];
|
|
69
|
-
let current = "";
|
|
70
|
-
let quote;
|
|
71
|
-
for (let index = 0; index < command.length; index += 1) {
|
|
72
|
-
const character = command[index];
|
|
73
|
-
const next = command[index + 1];
|
|
74
|
-
if (character === "\\" && quote !== "'" && next && (next === "\\" || next === quote || /\s/.test(next))) {
|
|
75
|
-
current += next;
|
|
76
|
-
index += 1;
|
|
77
|
-
}
|
|
78
|
-
else if ((character === "'" || character === '"') && (!quote || quote === character)) {
|
|
79
|
-
quote = quote ? undefined : character;
|
|
80
|
-
}
|
|
81
|
-
else if (!quote && /\s/.test(character)) {
|
|
82
|
-
if (current) {
|
|
83
|
-
parts.push(current);
|
|
84
|
-
current = "";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
current += character;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (quote)
|
|
92
|
-
throw new Error(`LiteLLM API key helper command has an unterminated quote: ${command}`);
|
|
93
|
-
if (current)
|
|
94
|
-
parts.push(current);
|
|
95
|
-
if (parts.length === 0)
|
|
96
|
-
throw new Error("LiteLLM API key helper command is empty");
|
|
97
|
-
if (/\.(?:cmd|bat)$/i.test(parts[0]))
|
|
98
|
-
throw new Error("LiteLLM API key helper shell scripts are not supported; use an executable");
|
|
99
|
-
return parts;
|
|
100
|
-
}
|
|
101
|
-
function executeApiKeyCommand(commandConfig) {
|
|
102
|
-
const [command, ...args] = parseApiKeyCommand(commandConfig);
|
|
103
|
-
const output = execFileSync(command, args, {
|
|
104
|
-
encoding: "utf8",
|
|
105
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
-
timeout: 10_000,
|
|
107
|
-
}).trim();
|
|
108
|
-
if (!output)
|
|
109
|
-
throw new Error(`LiteLLM API key helper produced no output: ${command}`);
|
|
110
|
-
return output;
|
|
111
|
-
}
|
|
112
|
-
function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
|
|
113
|
-
const [, payload] = apiKey.split(".");
|
|
114
|
-
if (!payload)
|
|
115
|
-
return opaqueFallback;
|
|
116
|
-
try {
|
|
117
|
-
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
118
|
-
return typeof claims.exp === "number"
|
|
119
|
-
? Math.max(Date.now(), claims.exp * 1000 - TOKEN_REFRESH_LEAD_MS)
|
|
120
|
-
: opaqueFallback;
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
return opaqueFallback;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function configuredCliJwtExpiresAt() {
|
|
127
|
-
const configured = Number(process.env[ENV_CLI_JWT_EXPIRATION_HOURS]);
|
|
128
|
-
const hours = Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_CLI_JWT_EXPIRATION_HOURS;
|
|
129
|
-
return Date.now() + hours * 60 * 60 * 1_000;
|
|
130
|
-
}
|
|
131
|
-
function boundedLoginSignal(signal) {
|
|
132
|
-
return signal
|
|
133
|
-
? AbortSignal.any([signal, AbortSignal.timeout(LOGIN_TIMEOUT_MS)])
|
|
134
|
-
: AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
135
|
-
}
|
|
136
|
-
async function generateVirtualKey(baseUrl, userToken, signal, headers) {
|
|
137
|
-
const response = await fetch(`${baseUrl}/key/generate`, {
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: {
|
|
140
|
-
...headers,
|
|
141
|
-
Authorization: `Bearer ${userToken}`,
|
|
142
|
-
"Content-Type": "application/json",
|
|
143
|
-
},
|
|
144
|
-
body: JSON.stringify({}),
|
|
145
|
-
signal: boundedLoginSignal(signal),
|
|
146
|
-
});
|
|
147
|
-
if (!response.ok) {
|
|
148
|
-
throw new Error(`Virtual key generation failed (${response.status})`);
|
|
149
|
-
}
|
|
150
|
-
const data = (await response.json());
|
|
151
|
-
if (typeof data.key !== "string" || !data.key)
|
|
152
|
-
throw new Error("No key in response from /key/generate");
|
|
153
|
-
const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
|
|
154
|
-
return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
|
|
155
|
-
}
|
|
156
|
-
function resolveTemplateConfigValue(config) {
|
|
157
|
-
let resolved = "";
|
|
158
|
-
for (let index = 0; index < config.length;) {
|
|
159
|
-
const dollarIndex = config.indexOf("$", index);
|
|
160
|
-
if (dollarIndex === -1)
|
|
161
|
-
return resolved + config.slice(index);
|
|
162
|
-
resolved += config.slice(index, dollarIndex);
|
|
163
|
-
const nextChar = config[dollarIndex + 1];
|
|
164
|
-
if (nextChar === "$" || nextChar === "!") {
|
|
165
|
-
resolved += nextChar;
|
|
166
|
-
index = dollarIndex + 2;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (nextChar === "{") {
|
|
170
|
-
const endIndex = config.indexOf("}", dollarIndex + 2);
|
|
171
|
-
if (endIndex === -1) {
|
|
172
|
-
resolved += "$";
|
|
173
|
-
index = dollarIndex + 1;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
const name = config.slice(dollarIndex + 2, endIndex);
|
|
177
|
-
const envValue = process.env[name];
|
|
178
|
-
if (envValue === undefined)
|
|
179
|
-
return undefined;
|
|
180
|
-
resolved += envValue;
|
|
181
|
-
index = endIndex + 1;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
const match = config.slice(dollarIndex + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
185
|
-
if (!match) {
|
|
186
|
-
resolved += "$";
|
|
187
|
-
index = dollarIndex + 1;
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
const envValue = process.env[match[0]];
|
|
191
|
-
if (envValue === undefined)
|
|
192
|
-
return undefined;
|
|
193
|
-
resolved += envValue;
|
|
194
|
-
index = dollarIndex + 1 + match[0].length;
|
|
195
|
-
}
|
|
196
|
-
return resolved;
|
|
197
|
-
}
|
|
198
|
-
async function resolveTemplateConfigValueFromContext(config, env) {
|
|
199
|
-
let resolved = "";
|
|
200
|
-
for (let index = 0; index < config.length;) {
|
|
201
|
-
const dollarIndex = config.indexOf("$", index);
|
|
202
|
-
if (dollarIndex === -1)
|
|
203
|
-
return resolved + config.slice(index);
|
|
204
|
-
resolved += config.slice(index, dollarIndex);
|
|
205
|
-
const nextChar = config[dollarIndex + 1];
|
|
206
|
-
if (nextChar === "$" || nextChar === "!") {
|
|
207
|
-
resolved += nextChar;
|
|
208
|
-
index = dollarIndex + 2;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (nextChar === "{") {
|
|
212
|
-
const endIndex = config.indexOf("}", dollarIndex + 2);
|
|
213
|
-
if (endIndex === -1) {
|
|
214
|
-
resolved += "$";
|
|
215
|
-
index = dollarIndex + 1;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const envValue = await env(config.slice(dollarIndex + 2, endIndex));
|
|
219
|
-
if (envValue === undefined)
|
|
220
|
-
return undefined;
|
|
221
|
-
resolved += envValue;
|
|
222
|
-
index = endIndex + 1;
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
const match = config.slice(dollarIndex + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
226
|
-
if (!match) {
|
|
227
|
-
resolved += "$";
|
|
228
|
-
index = dollarIndex + 1;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const envValue = await env(match[0]);
|
|
232
|
-
if (envValue === undefined)
|
|
233
|
-
return undefined;
|
|
234
|
-
resolved += envValue;
|
|
235
|
-
index = dollarIndex + 1 + match[0].length;
|
|
236
|
-
}
|
|
237
|
-
return resolved;
|
|
238
|
-
}
|
|
239
|
-
function resolveConfigValue(config, { executeCommands }) {
|
|
240
|
-
if (config.startsWith("!"))
|
|
241
|
-
return executeCommands ? executeApiKeyCommand(config) : undefined;
|
|
242
|
-
return resolveTemplateConfigValue(config);
|
|
243
|
-
}
|
|
244
|
-
const warnedUnresolvedApiKeys = new Set();
|
|
245
|
-
function warnUnresolvedApiKeyConfig(providerName, config) {
|
|
246
|
-
const key = `${providerName} ${config}`;
|
|
247
|
-
if (warnedUnresolvedApiKeys.has(key))
|
|
248
|
-
return;
|
|
249
|
-
warnedUnresolvedApiKeys.add(key);
|
|
250
|
-
process.stderr.write(`LiteLLM (${providerName}): configured apiKey did not resolve (unset environment variable?); use $$ for a literal $.\n`);
|
|
251
|
-
}
|
|
252
|
-
function parseHeaderRecord(value) {
|
|
253
|
-
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
254
|
-
return undefined;
|
|
255
|
-
const headers = {};
|
|
256
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
257
|
-
if (!key.trim())
|
|
258
|
-
continue;
|
|
259
|
-
let resolved;
|
|
260
|
-
if (typeof raw === "string")
|
|
261
|
-
resolved = resolveTemplateConfigValue(raw);
|
|
262
|
-
else if (typeof raw === "number" || typeof raw === "boolean")
|
|
263
|
-
resolved = String(raw);
|
|
264
|
-
else {
|
|
265
|
-
process.stderr.write(`LiteLLM: ignoring non-primitive header value for "${key}".\n`);
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (resolved)
|
|
269
|
-
headers[key] = resolved;
|
|
270
|
-
}
|
|
271
|
-
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
272
|
-
}
|
|
273
|
-
function parseCustomHeaders(raw) {
|
|
274
|
-
const trimmed = cleanConfig(raw);
|
|
275
|
-
if (!trimmed)
|
|
276
|
-
return undefined;
|
|
277
|
-
try {
|
|
278
|
-
return parseHeaderRecord(JSON.parse(trimmed));
|
|
279
|
-
}
|
|
280
|
-
catch (error) {
|
|
281
|
-
process.stderr.write(`LiteLLM: failed to parse custom headers (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function resolveHeaders(definition) {
|
|
286
|
-
if (typeof definition.headers === "string")
|
|
287
|
-
return parseCustomHeaders(resolveTemplateConfigValue(definition.headers));
|
|
288
|
-
return parseHeaderRecord(definition.headers);
|
|
289
|
-
}
|
|
290
|
-
async function resolveHeadersFromContext(definition, env) {
|
|
291
|
-
if (typeof definition.headers === "string")
|
|
292
|
-
return parseCustomHeaders(await resolveTemplateConfigValueFromContext(definition.headers, env));
|
|
293
|
-
return parseHeaderRecord(definition.headers);
|
|
294
|
-
}
|
|
295
|
-
async function resolveCredentials(definition, { executeHelpers = true } = {}) {
|
|
296
|
-
const configuredBase = cleanConfig(definition.baseUrl) ?? (definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined);
|
|
297
|
-
const envKey = definition.useDefaultEnv ? cleanConfig(process.env[ENV_API_KEY]) : undefined;
|
|
298
|
-
const envHelperCommand = definition.useDefaultEnv ? getApiKeyHelperCommand() : undefined;
|
|
299
|
-
const useGcloudToken = definition.useGcloudTokenAuth && isGcloudTokenAuthEnabled();
|
|
300
|
-
const gcloudCacheKey = useGcloudToken ? ((await getGcloudTokenCacheKey()) ?? undefined) : undefined;
|
|
301
|
-
const gcloudKey = executeHelpers && gcloudCacheKey ? (await getGcloudToken())?.trim() : undefined;
|
|
302
|
-
// Resolved lazily so a `!command` key is not executed when a
|
|
303
|
-
// higher-precedence credential (saved auth, gcloud token) already won.
|
|
304
|
-
let configuredKey;
|
|
305
|
-
if (!gcloudKey && definition.apiKeyConfig) {
|
|
306
|
-
configuredKey = resolveConfigValue(definition.apiKeyConfig, { executeCommands: executeHelpers });
|
|
307
|
-
if (configuredKey === undefined && !definition.apiKeyConfig.startsWith("!")) {
|
|
308
|
-
warnUnresolvedApiKeyConfig(definition.name, definition.apiKeyConfig);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const helperKey = !gcloudKey && !configuredKey && executeHelpers && envHelperCommand
|
|
312
|
-
? executeApiKeyCommand(envHelperCommand)
|
|
313
|
-
: undefined;
|
|
314
|
-
const apiKey = gcloudKey || configuredKey || helperKey || envKey;
|
|
315
|
-
let apiKeyConfig;
|
|
316
|
-
if (gcloudKey) {
|
|
317
|
-
apiKeyConfig = getGcloudTokenCommand();
|
|
318
|
-
}
|
|
319
|
-
else if (!executeHelpers && gcloudCacheKey) {
|
|
320
|
-
apiKeyConfig = getGcloudTokenCommand();
|
|
321
|
-
}
|
|
322
|
-
else if (configuredKey && definition.apiKeyConfig) {
|
|
323
|
-
apiKeyConfig = definition.apiKeyConfig;
|
|
324
|
-
}
|
|
325
|
-
else if (!executeHelpers && definition.apiKeyConfig?.startsWith("!")) {
|
|
326
|
-
apiKeyConfig = definition.apiKeyConfig;
|
|
327
|
-
}
|
|
328
|
-
else if (helperKey && envHelperCommand) {
|
|
329
|
-
apiKeyConfig = envHelperCommand;
|
|
330
|
-
}
|
|
331
|
-
else if (!executeHelpers && envHelperCommand) {
|
|
332
|
-
apiKeyConfig = envHelperCommand;
|
|
333
|
-
}
|
|
334
|
-
else if (envKey) {
|
|
335
|
-
apiKeyConfig = `$${ENV_API_KEY}`;
|
|
336
|
-
}
|
|
337
|
-
return {
|
|
338
|
-
baseUrl: configuredBase ? normalizeBaseUrl(configuredBase, definition.allowInsecureHttp) : undefined,
|
|
339
|
-
apiKey: apiKey || undefined,
|
|
340
|
-
apiKeyConfig,
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
function getDiscoveryTimeoutMs() {
|
|
344
|
-
const raw = process.env[ENV_TIMEOUT];
|
|
345
|
-
if (raw === undefined)
|
|
346
|
-
return DEFAULT_TIMEOUT_MS;
|
|
347
|
-
const parsed = Number.parseInt(raw, 10);
|
|
348
|
-
if (Number.isNaN(parsed) || parsed < 0)
|
|
349
|
-
return DEFAULT_TIMEOUT_MS;
|
|
350
|
-
return parsed;
|
|
351
|
-
}
|
|
352
|
-
function isOffline() {
|
|
353
|
-
return process.env[ENV_OFFLINE] === "1";
|
|
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
|
-
}
|
|
368
|
-
function isVerboseDiscovery() {
|
|
369
|
-
return process.env[ENV_VERBOSE_DISCOVERY] === "1";
|
|
370
|
-
}
|
|
371
|
-
function normalizeProviderSettings(raw) {
|
|
372
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
373
|
-
return undefined;
|
|
374
|
-
const record = raw;
|
|
375
|
-
if (record.enabled === false)
|
|
376
|
-
return undefined;
|
|
377
|
-
return record;
|
|
378
|
-
}
|
|
379
|
-
function isFeatureEnabled(settings, feature) {
|
|
380
|
-
const raw = settings?.[feature];
|
|
381
|
-
return !isPlainObject(raw) || raw.enabled !== false;
|
|
382
|
-
}
|
|
383
|
-
function getProviderDefinitions(settings) {
|
|
384
|
-
const rawProviders = settings?.providers && typeof settings.providers === "object" ? settings.providers : undefined;
|
|
385
|
-
const providerSettings = rawProviders;
|
|
386
|
-
const defaultSettings = normalizeProviderSettings(providerSettings?.[PROVIDER_NAME]);
|
|
387
|
-
const makeDefinition = (name, raw, isDefault) => ({
|
|
388
|
-
name,
|
|
389
|
-
displayName: stringSetting(raw?.displayName) ?? (isDefault ? "LiteLLM" : name),
|
|
390
|
-
baseUrl: stringSetting(raw?.baseUrl),
|
|
391
|
-
apiKeyConfig: stringSetting(raw?.apiKey),
|
|
392
|
-
headers: raw?.headers ?? (isDefault ? `$${ENV_HEADERS}` : undefined),
|
|
393
|
-
useDefaultEnv: isDefault,
|
|
394
|
-
useGcloudTokenAuth: isDefault,
|
|
395
|
-
enableOAuth: isDefault,
|
|
396
|
-
allowInsecureHttp: raw?.allowInsecureHttp === true,
|
|
397
|
-
});
|
|
398
|
-
const definitions = [makeDefinition(PROVIDER_NAME, defaultSettings, true)];
|
|
399
|
-
for (const [name, raw] of Object.entries(providerSettings ?? {})) {
|
|
400
|
-
if (name === PROVIDER_NAME)
|
|
401
|
-
continue;
|
|
402
|
-
const normalized = normalizeProviderSettings(raw);
|
|
403
|
-
if (!normalized)
|
|
404
|
-
continue;
|
|
405
|
-
definitions.push(makeDefinition(name, normalized, false));
|
|
406
|
-
}
|
|
407
|
-
return definitions;
|
|
408
|
-
}
|
|
409
|
-
const DIFFERENT_BASE_URL = "enter-a-different-url";
|
|
410
|
-
/**
|
|
411
|
-
* The proxy URL is the one thing a user has to retype on every re-login, and an expired SSO token is
|
|
412
|
-
* the common reason to re-login. Offer whatever we already know: settings, env, or the credential the
|
|
413
|
-
* dead session was issued against.
|
|
414
|
-
*/
|
|
415
|
-
function knownBaseUrl(definition) {
|
|
416
|
-
const stored = readStoredCredential(definition.name, join(getAgentDir(), "auth.json"));
|
|
417
|
-
const candidates = [
|
|
418
|
-
[cleanConfig(definition.baseUrl), "configured"],
|
|
419
|
-
[definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined, `$${ENV_BASE_URL}`],
|
|
420
|
-
[
|
|
421
|
-
stored?.type === "oauth"
|
|
422
|
-
? cleanConfig(typeof stored.baseUrl === "string" ? stored.baseUrl : undefined)
|
|
423
|
-
: cleanConfig(stored?.env?.[ENV_BASE_URL]),
|
|
424
|
-
"previous login",
|
|
425
|
-
],
|
|
426
|
-
];
|
|
427
|
-
for (const [candidate, source] of candidates) {
|
|
428
|
-
if (!candidate)
|
|
429
|
-
continue;
|
|
430
|
-
try {
|
|
431
|
-
return { url: normalizeBaseUrl(candidate, definition.allowInsecureHttp), source };
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
// A URL we could not use anyway is not worth offering; try the next candidate.
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
async function promptBaseUrl(interaction, definition) {
|
|
440
|
-
const known = knownBaseUrl(definition);
|
|
441
|
-
if (known) {
|
|
442
|
-
const choice = await interaction.prompt({
|
|
443
|
-
type: "select",
|
|
444
|
-
message: "LiteLLM proxy URL:",
|
|
445
|
-
// Pi's login selector renders labels only, so the source has to ride along in the label.
|
|
446
|
-
options: [
|
|
447
|
-
{ id: known.url, label: `${known.url} (${known.source})` },
|
|
448
|
-
{ id: DIFFERENT_BASE_URL, label: "Enter a different URL…" },
|
|
449
|
-
],
|
|
450
|
-
});
|
|
451
|
-
if (choice === known.url)
|
|
452
|
-
return known.url;
|
|
453
|
-
}
|
|
454
|
-
const rawBaseUrl = (await interaction.prompt({
|
|
455
|
-
type: "text",
|
|
456
|
-
message: "Enter LiteLLM proxy URL (no trailing /v1):",
|
|
457
|
-
placeholder: "https://litellm.example.com",
|
|
458
|
-
})).trim();
|
|
459
|
-
if (!rawBaseUrl)
|
|
460
|
-
throw new Error("Base URL is required");
|
|
461
|
-
return normalizeBaseUrl(rawBaseUrl, definition.allowInsecureHttp);
|
|
462
|
-
}
|
|
463
|
-
async function loginApiKey(interaction, definition) {
|
|
464
|
-
const baseUrl = await promptBaseUrl(interaction, definition);
|
|
465
|
-
const key = (await interaction.prompt({ type: "secret", message: "Enter API key:" })).trim();
|
|
466
|
-
if (!key)
|
|
467
|
-
throw new Error("Both base URL and API key are required");
|
|
468
|
-
return { type: "api_key", key, env: { [ENV_BASE_URL]: baseUrl } };
|
|
469
|
-
}
|
|
470
|
-
async function startCliSso(baseUrl, signal, headers) {
|
|
471
|
-
const response = await fetch(`${baseUrl}/sso/cli/start`, {
|
|
472
|
-
method: "POST",
|
|
473
|
-
headers,
|
|
474
|
-
signal: boundedLoginSignal(signal),
|
|
475
|
-
});
|
|
476
|
-
if (response.status === 404 || response.status === 405)
|
|
477
|
-
return undefined;
|
|
478
|
-
if (!response.ok)
|
|
479
|
-
throw new Error(`LiteLLM CLI SSO start failed (HTTP ${response.status})`);
|
|
480
|
-
const data = (await response.json());
|
|
481
|
-
if (typeof data.login_id !== "string" ||
|
|
482
|
-
!data.login_id ||
|
|
483
|
-
typeof data.poll_secret !== "string" ||
|
|
484
|
-
!data.poll_secret ||
|
|
485
|
-
typeof data.user_code !== "string" ||
|
|
486
|
-
!data.user_code) {
|
|
487
|
-
throw new Error("LiteLLM CLI SSO start returned an invalid response");
|
|
488
|
-
}
|
|
489
|
-
return {
|
|
490
|
-
loginId: data.login_id,
|
|
491
|
-
pollSecret: data.poll_secret,
|
|
492
|
-
userCode: data.user_code,
|
|
493
|
-
expiresInSeconds: typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in > 0
|
|
494
|
-
? data.expires_in
|
|
495
|
-
: CLI_SSO_EXPIRES_IN_SECONDS,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function waitForNextCliSsoPoll(interaction) {
|
|
499
|
-
try {
|
|
500
|
-
await delay(CLI_SSO_POLL_INTERVAL_MS, undefined, interaction.signal ? { signal: interaction.signal } : undefined);
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
if (interaction.signal?.aborted)
|
|
504
|
-
throw interaction.signal.reason;
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async function pollCliSso(baseUrl, start, interaction, headers) {
|
|
509
|
-
const deadline = Date.now() + start.expiresInSeconds * 1_000;
|
|
510
|
-
const pollUrl = new URL(`${baseUrl}/sso/cli/poll/${encodeURIComponent(start.loginId)}`);
|
|
511
|
-
let teamId;
|
|
512
|
-
while (Date.now() < deadline) {
|
|
513
|
-
if (teamId)
|
|
514
|
-
pollUrl.searchParams.set("team_id", teamId);
|
|
515
|
-
let response;
|
|
516
|
-
try {
|
|
517
|
-
response = await fetch(pollUrl, {
|
|
518
|
-
headers: { ...headers, "x-litellm-cli-poll-secret": start.pollSecret },
|
|
519
|
-
signal: boundedLoginSignal(interaction.signal),
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
if (interaction.signal?.aborted)
|
|
524
|
-
throw interaction.signal.reason;
|
|
525
|
-
await waitForNextCliSsoPoll(interaction);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
if (response.status === 429 || response.status >= 500) {
|
|
529
|
-
await waitForNextCliSsoPoll(interaction);
|
|
530
|
-
continue;
|
|
531
|
-
}
|
|
532
|
-
if (response.status === 400)
|
|
533
|
-
throw new Error("LiteLLM CLI SSO login expired or is invalid");
|
|
534
|
-
if (!response.ok)
|
|
535
|
-
throw new Error(`LiteLLM CLI SSO polling failed (HTTP ${response.status})`);
|
|
536
|
-
const data = (await response.json());
|
|
537
|
-
if (data.status === "ready" && typeof data.key === "string" && data.key) {
|
|
538
|
-
return {
|
|
539
|
-
access: data.key,
|
|
540
|
-
expiresInSeconds: typeof data.expires_in === "number" && Number.isFinite(data.expires_in) && data.expires_in > 0
|
|
541
|
-
? data.expires_in
|
|
542
|
-
: undefined,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
if (data.status === "ready" && data.requires_team_selection === true && !teamId) {
|
|
546
|
-
const details = Array.isArray(data.team_details) ? data.team_details : [];
|
|
547
|
-
const teams = details.flatMap((team) => {
|
|
548
|
-
if (!team || typeof team !== "object")
|
|
549
|
-
return [];
|
|
550
|
-
const record = team;
|
|
551
|
-
const id = typeof record.team_id === "string" ? record.team_id : record.id;
|
|
552
|
-
const alias = record.team_alias;
|
|
553
|
-
return typeof id === "string" && id ? [{ id, label: typeof alias === "string" && alias ? alias : id }] : [];
|
|
554
|
-
});
|
|
555
|
-
if (teams.length === 0 && Array.isArray(data.teams))
|
|
556
|
-
teams.push(...data.teams.flatMap((id) => (typeof id === "string" && id ? [{ id, label: id }] : [])));
|
|
557
|
-
if (teams.length === 0)
|
|
558
|
-
throw new Error("LiteLLM CLI SSO requested team selection without any teams");
|
|
559
|
-
const selected = await interaction.prompt({ type: "select", message: "Select a LiteLLM team:", options: teams });
|
|
560
|
-
if (!teams.some((team) => team.id === selected))
|
|
561
|
-
throw new Error("Invalid LiteLLM team selection");
|
|
562
|
-
teamId = selected;
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
if (data.status !== "pending")
|
|
566
|
-
throw new Error("LiteLLM CLI SSO polling returned an invalid response");
|
|
567
|
-
await waitForNextCliSsoPoll(interaction);
|
|
568
|
-
}
|
|
569
|
-
throw new Error("LiteLLM CLI SSO login expired");
|
|
570
|
-
}
|
|
571
|
-
async function loginWithPastedToken(interaction, baseUrl, headers) {
|
|
572
|
-
interaction.notify({
|
|
573
|
-
type: "auth_url",
|
|
574
|
-
url: `${baseUrl}/sso/key/generate`,
|
|
575
|
-
instructions: "Authenticate via SSO, then copy your token from the LiteLLM UI.",
|
|
576
|
-
});
|
|
577
|
-
const rawToken = (await interaction.prompt({ type: "secret", message: "Paste your SSO token from the LiteLLM UI:" }))
|
|
578
|
-
.trim()
|
|
579
|
-
.replace(/^Bearer\s+/i, "")
|
|
580
|
-
.trim();
|
|
581
|
-
if (!rawToken)
|
|
582
|
-
throw new Error("SSO token is required");
|
|
583
|
-
const wantVirtualKey = (await interaction.prompt({ type: "text", message: "Generate a LiteLLM virtual key from this token? (y/n):" }))
|
|
584
|
-
.trim()
|
|
585
|
-
.toLowerCase();
|
|
586
|
-
let access = rawToken;
|
|
587
|
-
let expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
588
|
-
if (wantVirtualKey !== "n" && wantVirtualKey !== "no") {
|
|
589
|
-
try {
|
|
590
|
-
interaction.notify({ type: "progress", message: "Generating virtual key..." });
|
|
591
|
-
const generated = await generateVirtualKey(baseUrl, rawToken, interaction.signal, headers);
|
|
592
|
-
access = generated.key;
|
|
593
|
-
expires =
|
|
594
|
-
generated.expiresAt === undefined
|
|
595
|
-
? PERMANENT_TOKEN_EXPIRES_AT
|
|
596
|
-
: Math.max(Date.now(), generated.expiresAt - TOKEN_REFRESH_LEAD_MS);
|
|
597
|
-
interaction.notify({ type: "progress", message: "Virtual key generated and will be used for API calls." });
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
if (interaction.signal?.aborted)
|
|
601
|
-
throw interaction.signal.reason;
|
|
602
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
603
|
-
interaction.notify({
|
|
604
|
-
type: "progress",
|
|
605
|
-
message: `LiteLLM: virtual key generation failed (${message}); using SSO token directly.`,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return { type: "oauth", access, refresh: "", expires, baseUrl };
|
|
610
|
-
}
|
|
611
|
-
async function loginOAuth(interaction, definition) {
|
|
612
|
-
const headers = resolveHeaders(definition);
|
|
613
|
-
const baseUrl = await promptBaseUrl(interaction, definition);
|
|
614
|
-
const cliSso = await startCliSso(baseUrl, interaction.signal, headers);
|
|
615
|
-
if (!cliSso)
|
|
616
|
-
return loginWithPastedToken(interaction, baseUrl, headers);
|
|
617
|
-
interaction.notify({
|
|
618
|
-
type: "device_code",
|
|
619
|
-
userCode: cliSso.userCode,
|
|
620
|
-
verificationUri: `${baseUrl}/sso/key/generate?${new URLSearchParams({ source: "litellm-cli", key: cliSso.loginId })}`,
|
|
621
|
-
expiresInSeconds: cliSso.expiresInSeconds,
|
|
622
|
-
});
|
|
623
|
-
const result = await pollCliSso(baseUrl, cliSso, interaction, headers);
|
|
624
|
-
return {
|
|
625
|
-
type: "oauth",
|
|
626
|
-
access: result.access,
|
|
627
|
-
refresh: "",
|
|
628
|
-
expires: result.expiresInSeconds
|
|
629
|
-
? Date.now() + result.expiresInSeconds * 1_000
|
|
630
|
-
: tokenExpiresAt(result.access, configuredCliJwtExpiresAt()),
|
|
631
|
-
baseUrl,
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
async function refreshLiteLLM(credentials, _signal) {
|
|
635
|
-
if (!credentials.refresh.startsWith("!")) {
|
|
636
|
-
if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
|
|
637
|
-
throw new Error("LiteLLM credential cannot be refreshed; run /login litellm again");
|
|
638
|
-
}
|
|
639
|
-
return credentials;
|
|
640
|
-
}
|
|
641
|
-
const access = executeApiKeyCommand(credentials.refresh);
|
|
642
|
-
return { ...credentials, access, expires: tokenExpiresAt(access, EXPIRE_TOKEN_IMMEDIATELY) };
|
|
643
|
-
}
|
|
644
|
-
async function resolveApiKeyAuth(definition, ctx, credential, executeHelpers = true) {
|
|
645
|
-
const baseUrl = cleanConfig(credential?.env?.[ENV_BASE_URL]) ??
|
|
646
|
-
cleanConfig(definition.baseUrl) ??
|
|
647
|
-
(definition.useDefaultEnv ? cleanConfig(await ctx.env(ENV_BASE_URL)) : undefined);
|
|
648
|
-
const stored = credential?.key
|
|
649
|
-
? resolveConfigValue(credential.key, { executeCommands: executeHelpers })?.trim()
|
|
650
|
-
: undefined;
|
|
651
|
-
let source;
|
|
652
|
-
let creds;
|
|
653
|
-
if (stored) {
|
|
654
|
-
source = "stored credential";
|
|
655
|
-
creds = {
|
|
656
|
-
baseUrl: baseUrl ? normalizeBaseUrl(baseUrl, definition.allowInsecureHttp) : undefined,
|
|
657
|
-
apiKey: stored,
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
else {
|
|
661
|
-
creds = await resolveCredentials({ ...definition, apiKeyConfig: undefined, useDefaultEnv: false }, { executeHelpers });
|
|
662
|
-
if (!creds.apiKey && definition.apiKeyConfig) {
|
|
663
|
-
const configured = definition.apiKeyConfig.startsWith("!")
|
|
664
|
-
? executeHelpers
|
|
665
|
-
? executeApiKeyCommand(definition.apiKeyConfig)
|
|
666
|
-
: undefined
|
|
667
|
-
: await resolveTemplateConfigValueFromContext(definition.apiKeyConfig, ctx.env);
|
|
668
|
-
if (configured) {
|
|
669
|
-
creds.apiKey = configured;
|
|
670
|
-
creds.apiKeyConfig = definition.apiKeyConfig;
|
|
671
|
-
}
|
|
672
|
-
else if (!definition.apiKeyConfig.startsWith("!")) {
|
|
673
|
-
warnUnresolvedApiKeyConfig(definition.name, definition.apiKeyConfig);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
if (!creds.apiKey && definition.useDefaultEnv) {
|
|
677
|
-
const helper = normalizeCommand(await ctx.env(ENV_API_KEY_HELPER));
|
|
678
|
-
if (helper) {
|
|
679
|
-
creds.apiKey = executeHelpers ? executeApiKeyCommand(helper) : undefined;
|
|
680
|
-
creds.apiKeyConfig = helper;
|
|
681
|
-
source = ENV_API_KEY_HELPER;
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
const envKey = cleanConfig(await ctx.env(ENV_API_KEY));
|
|
685
|
-
if (envKey) {
|
|
686
|
-
creds.apiKey = envKey;
|
|
687
|
-
creds.apiKeyConfig = ENV_API_KEY;
|
|
688
|
-
source = ENV_API_KEY;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
if (!creds.baseUrl && baseUrl)
|
|
693
|
-
creds.baseUrl = normalizeBaseUrl(baseUrl, definition.allowInsecureHttp);
|
|
694
|
-
}
|
|
695
|
-
if (!creds.apiKey)
|
|
696
|
-
return undefined;
|
|
697
|
-
return {
|
|
698
|
-
auth: {
|
|
699
|
-
apiKey: creds.apiKey,
|
|
700
|
-
baseUrl: creds.baseUrl ? `${creds.baseUrl}/v1` : undefined,
|
|
701
|
-
headers: await resolveHeadersFromContext(definition, ctx.env),
|
|
702
|
-
},
|
|
703
|
-
env: baseUrl ? { [ENV_BASE_URL]: normalizeBaseUrl(baseUrl, definition.allowInsecureHttp) } : undefined,
|
|
704
|
-
source: source ?? creds.apiKeyConfig ?? ENV_API_KEY,
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
function createProviderAuth(definition) {
|
|
708
|
-
return {
|
|
709
|
-
apiKey: {
|
|
710
|
-
name: `${definition.displayName} API key`,
|
|
711
|
-
login: definition.name === PROVIDER_NAME ? (interaction) => loginApiKey(interaction, definition) : undefined,
|
|
712
|
-
check: async ({ ctx, credential }) => {
|
|
713
|
-
const baseUrl = credential?.env?.[ENV_BASE_URL] ??
|
|
714
|
-
definition.baseUrl ??
|
|
715
|
-
(definition.useDefaultEnv ? await ctx.env(ENV_BASE_URL) : undefined);
|
|
716
|
-
if (!cleanConfig(baseUrl))
|
|
717
|
-
return undefined;
|
|
718
|
-
if (credential?.key)
|
|
719
|
-
return { type: "api_key", source: "stored credential" };
|
|
720
|
-
const configured = await resolveCredentials({ ...definition, apiKeyConfig: undefined, useDefaultEnv: false }, { executeHelpers: false });
|
|
721
|
-
if (configured.apiKey || configured.apiKeyConfig)
|
|
722
|
-
return {
|
|
723
|
-
type: "api_key",
|
|
724
|
-
source: definition.useGcloudTokenAuth && isGcloudTokenAuthEnabled()
|
|
725
|
-
? "gcloud ADC"
|
|
726
|
-
: (configured.apiKeyConfig ?? ENV_API_KEY),
|
|
727
|
-
};
|
|
728
|
-
if (definition.apiKeyConfig) {
|
|
729
|
-
const configuredKey = definition.apiKeyConfig.startsWith("!")
|
|
730
|
-
? definition.apiKeyConfig
|
|
731
|
-
: await resolveTemplateConfigValueFromContext(definition.apiKeyConfig, ctx.env);
|
|
732
|
-
if (configuredKey)
|
|
733
|
-
return { type: "api_key", source: definition.apiKeyConfig };
|
|
734
|
-
}
|
|
735
|
-
if (definition.useDefaultEnv && cleanConfig(await ctx.env(ENV_API_KEY_HELPER)))
|
|
736
|
-
return { type: "api_key", source: ENV_API_KEY_HELPER };
|
|
737
|
-
return definition.useDefaultEnv && cleanConfig(await ctx.env(ENV_API_KEY))
|
|
738
|
-
? { type: "api_key", source: ENV_API_KEY }
|
|
739
|
-
: undefined;
|
|
740
|
-
},
|
|
741
|
-
resolve: ({ ctx, credential }) => resolveApiKeyAuth(definition, ctx, credential),
|
|
742
|
-
},
|
|
743
|
-
oauth: definition.enableOAuth
|
|
744
|
-
? {
|
|
745
|
-
name: "LiteLLM SSO",
|
|
746
|
-
loginLabel: "Sign in with LiteLLM SSO",
|
|
747
|
-
login: (interaction) => loginOAuth(interaction, definition),
|
|
748
|
-
refresh: async (credential, signal) => ({
|
|
749
|
-
...(await refreshLiteLLM(credential, signal)),
|
|
750
|
-
type: "oauth",
|
|
751
|
-
}),
|
|
752
|
-
toAuth: async (credential) => ({
|
|
753
|
-
apiKey: credential.access,
|
|
754
|
-
baseUrl: credential.baseUrl
|
|
755
|
-
? `${normalizeBaseUrl(String(credential.baseUrl), definition.allowInsecureHttp)}/v1`
|
|
756
|
-
: undefined,
|
|
757
|
-
headers: resolveHeaders(definition),
|
|
758
|
-
}),
|
|
759
|
-
}
|
|
760
|
-
: undefined,
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
function isReasoningItem(item) {
|
|
764
|
-
return typeof item === "object" && item !== null && item.type === "reasoning";
|
|
765
|
-
}
|
|
766
|
-
// Returns the original array reference when no normalization is needed.
|
|
767
|
-
function normalizeStrictToolMessages(messages) {
|
|
768
|
-
const normalized = [];
|
|
769
|
-
let imageParts = [];
|
|
770
|
-
let changed = false;
|
|
771
|
-
const flushImages = () => {
|
|
772
|
-
if (imageParts.length === 0)
|
|
773
|
-
return;
|
|
774
|
-
normalized.push({
|
|
775
|
-
role: "user",
|
|
776
|
-
content: [{ type: "text", text: "Attached image(s) from tool result:" }, ...imageParts],
|
|
777
|
-
});
|
|
778
|
-
imageParts = [];
|
|
779
|
-
};
|
|
780
|
-
for (const message of messages) {
|
|
781
|
-
if (message.role !== "tool")
|
|
782
|
-
flushImages();
|
|
783
|
-
if (message.role === "assistant" &&
|
|
784
|
-
Array.isArray(message.tool_calls) &&
|
|
785
|
-
message.tool_calls.length > 0 &&
|
|
786
|
-
message.content == null) {
|
|
787
|
-
normalized.push({ ...message, content: "" });
|
|
788
|
-
changed = true;
|
|
789
|
-
}
|
|
790
|
-
else if (message.role === "tool" && Array.isArray(message.content)) {
|
|
791
|
-
const previousImageCount = imageParts.length;
|
|
792
|
-
const content = message.content
|
|
793
|
-
.map((part) => {
|
|
794
|
-
if (typeof part === "string")
|
|
795
|
-
return part;
|
|
796
|
-
if (part?.type === "text")
|
|
797
|
-
return part.text || "";
|
|
798
|
-
if (part?.type === "image_url")
|
|
799
|
-
imageParts.push(part);
|
|
800
|
-
return "";
|
|
801
|
-
})
|
|
802
|
-
.join("");
|
|
803
|
-
normalized.push({
|
|
804
|
-
...message,
|
|
805
|
-
content: content || (imageParts.length > previousImageCount ? "(see attached image)" : "(no tool output)"),
|
|
806
|
-
});
|
|
807
|
-
changed = true;
|
|
808
|
-
}
|
|
809
|
-
else {
|
|
810
|
-
normalized.push(message);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
flushImages();
|
|
814
|
-
return changed ? normalized : messages;
|
|
815
|
-
}
|
|
816
|
-
// Reasoning fields LiteLLM forwards to chat-completions providers. The Moonshot
|
|
817
|
-
// path defaults them off; the gpt-5.5 tool path strips them entirely.
|
|
818
|
-
const REASONING_SUPPRESSION_DEFAULTS = {
|
|
819
|
-
include_reasoning: false,
|
|
820
|
-
reasoning_content: false,
|
|
821
|
-
merge_reasoning_content_in_choices: true,
|
|
822
|
-
thinking: { type: "disabled" },
|
|
823
|
-
};
|
|
824
|
-
const GEMINI_MODEL_PATTERN = /(?:^|[-_/.:])gemini(?=$|[-_/.:])/i;
|
|
825
|
-
function prepareLiteLLMRequestPayload(payload, model, sessionId) {
|
|
826
|
-
const modelId = model?.id;
|
|
827
|
-
const api = model?.api;
|
|
828
|
-
let next;
|
|
829
|
-
const update = (key, value) => {
|
|
830
|
-
if (payload[key] !== undefined)
|
|
831
|
-
return;
|
|
832
|
-
next ??= { ...payload };
|
|
833
|
-
next[key] = value;
|
|
834
|
-
};
|
|
835
|
-
if (api !== "openai-responses" && model?.suppressReasoningContent === true) {
|
|
836
|
-
for (const [key, value] of Object.entries(REASONING_SUPPRESSION_DEFAULTS)) {
|
|
837
|
-
if (key !== "thinking")
|
|
838
|
-
update(key, value);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
// LiteLLM still routes gpt-5.5 tool+reasoning requests through chat completions.
|
|
842
|
-
// Drop reasoning until the gateway honors /v1/responses for this route.
|
|
843
|
-
if (api !== "openai-responses" &&
|
|
844
|
-
modelId &&
|
|
845
|
-
isGpt55Model(modelId) &&
|
|
846
|
-
Array.isArray(payload.tools) &&
|
|
847
|
-
payload.tools.length > 0) {
|
|
848
|
-
const reasoningKeys = ["reasoning", "reasoning_effort", ...Object.keys(REASONING_SUPPRESSION_DEFAULTS)];
|
|
849
|
-
for (const key of reasoningKeys) {
|
|
850
|
-
if (payload[key] === undefined)
|
|
851
|
-
continue;
|
|
852
|
-
next ??= { ...payload };
|
|
853
|
-
delete next[key];
|
|
854
|
-
}
|
|
855
|
-
const include = (next ?? payload).include;
|
|
856
|
-
if (Array.isArray(include) && include.includes("reasoning.encrypted_content")) {
|
|
857
|
-
next ??= { ...payload };
|
|
858
|
-
const filteredInclude = include.filter((value) => value !== "reasoning.encrypted_content");
|
|
859
|
-
if (filteredInclude.length === 0)
|
|
860
|
-
delete next.include;
|
|
861
|
-
else
|
|
862
|
-
next.include = filteredInclude;
|
|
863
|
-
}
|
|
864
|
-
// Prior turns may have replayed reasoning items (with encrypted_content)
|
|
865
|
-
// into the input; they are rejected once reasoning is stripped.
|
|
866
|
-
const input = (next ?? payload).input;
|
|
867
|
-
if (Array.isArray(input) && input.some(isReasoningItem)) {
|
|
868
|
-
next ??= { ...payload };
|
|
869
|
-
next.input = input.filter((item) => !isReasoningItem(item));
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
// Moonshot/Kimi applies strict OpenAI schema validation: assistant tool calls
|
|
873
|
-
// must carry string content, and tool results must be plain text.
|
|
874
|
-
if (modelId && isMoonshotModel(modelId)) {
|
|
875
|
-
const messages = (next ?? payload).messages;
|
|
876
|
-
if (Array.isArray(messages)) {
|
|
877
|
-
const normalized = normalizeStrictToolMessages(messages);
|
|
878
|
-
if (normalized !== messages) {
|
|
879
|
-
next ??= { ...payload };
|
|
880
|
-
next.messages = normalized;
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if (modelId && GEMINI_MODEL_PATTERN.test(modelId)) {
|
|
885
|
-
const currentPayload = next ?? payload;
|
|
886
|
-
if (typeof currentPayload.reasoning_effort === "string") {
|
|
887
|
-
const lower = currentPayload.reasoning_effort.toLowerCase();
|
|
888
|
-
if (currentPayload.reasoning_effort !== lower) {
|
|
889
|
-
next ??= { ...payload };
|
|
890
|
-
next.reasoning_effort = lower;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
const reasoningPayload = next ?? payload;
|
|
894
|
-
if (isPlainObject(reasoningPayload.reasoning) && typeof reasoningPayload.reasoning.effort === "string") {
|
|
895
|
-
const lower = reasoningPayload.reasoning.effort.toLowerCase();
|
|
896
|
-
if (reasoningPayload.reasoning.effort !== lower) {
|
|
897
|
-
next ??= { ...payload };
|
|
898
|
-
next.reasoning = {
|
|
899
|
-
...next.reasoning,
|
|
900
|
-
effort: lower,
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
if (sessionId) {
|
|
906
|
-
next ??= { ...payload };
|
|
907
|
-
next.litellm_session_id = sessionId;
|
|
908
|
-
}
|
|
909
|
-
return next;
|
|
910
|
-
}
|
|
911
|
-
function normalizeThinkTags(message, litellmProviderNames) {
|
|
912
|
-
if (!litellmProviderNames.has(message.provider) || !emitsThinkTags(message.model))
|
|
913
|
-
return;
|
|
914
|
-
let changed = false;
|
|
915
|
-
const content = [];
|
|
916
|
-
const appendText = (text) => {
|
|
917
|
-
if (!text)
|
|
918
|
-
return;
|
|
919
|
-
const last = content.at(-1);
|
|
920
|
-
if (last?.type === "text") {
|
|
921
|
-
last.text += text;
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
content.push({ type: "text", text });
|
|
925
|
-
};
|
|
926
|
-
const appendThinking = (thinking) => {
|
|
927
|
-
if (!thinking)
|
|
928
|
-
return;
|
|
929
|
-
const last = content.at(-1);
|
|
930
|
-
if (last?.type === "thinking") {
|
|
931
|
-
last.thinking += thinking;
|
|
932
|
-
return;
|
|
933
|
-
}
|
|
934
|
-
content.push({ type: "thinking", thinking });
|
|
935
|
-
};
|
|
936
|
-
for (let blockIndex = 0; blockIndex < message.content.length; blockIndex++) {
|
|
937
|
-
const block = message.content[blockIndex];
|
|
938
|
-
if (block.type !== "text") {
|
|
939
|
-
content.push(block);
|
|
940
|
-
continue;
|
|
941
|
-
}
|
|
942
|
-
let index = 0;
|
|
943
|
-
while (index < block.text.length) {
|
|
944
|
-
const start = block.text.indexOf("<think>", index);
|
|
945
|
-
if (start === -1) {
|
|
946
|
-
appendText(block.text.slice(index));
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
changed = true;
|
|
950
|
-
appendText(block.text.slice(index, start));
|
|
951
|
-
const thinkingStart = start + "<think>".length;
|
|
952
|
-
const end = block.text.indexOf("</think>", thinkingStart);
|
|
953
|
-
if (end === -1) {
|
|
954
|
-
const isBeforeNonTextContent = message.content
|
|
955
|
-
.slice(blockIndex + 1)
|
|
956
|
-
.some((nextBlock) => nextBlock.type !== "text");
|
|
957
|
-
if (isBeforeNonTextContent)
|
|
958
|
-
appendThinking(block.text.slice(thinkingStart));
|
|
959
|
-
else
|
|
960
|
-
appendText(block.text.slice(thinkingStart));
|
|
961
|
-
index = block.text.length;
|
|
962
|
-
break;
|
|
963
|
-
}
|
|
964
|
-
appendThinking(block.text.slice(thinkingStart, end));
|
|
965
|
-
index = end + "</think>".length;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
if (!changed)
|
|
969
|
-
return;
|
|
970
|
-
return { ...message, content };
|
|
971
|
-
}
|
|
972
|
-
export default async function (pi) {
|
|
973
|
-
const settings = await readGlobalLiteLLMSettings();
|
|
974
|
-
const definitions = getProviderDefinitions(settings);
|
|
975
|
-
const skillsEnabled = isFeatureEnabled(settings, "skills");
|
|
976
|
-
const mcpEnabled = isFeatureEnabled(settings, "mcp");
|
|
977
|
-
const providerNames = new Set(definitions.map((definition) => definition.name));
|
|
978
|
-
function discoveryDisabledReason() {
|
|
979
|
-
if (isOffline())
|
|
980
|
-
return `${ENV_OFFLINE}=1`;
|
|
981
|
-
if (getDiscoveryTimeoutMs() === 0)
|
|
982
|
-
return `${ENV_TIMEOUT}=0`;
|
|
983
|
-
return null;
|
|
984
|
-
}
|
|
985
|
-
function requestBaseUrl(definition) {
|
|
986
|
-
const baseUrl = cleanConfig(definition.baseUrl) ??
|
|
987
|
-
(definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined) ??
|
|
988
|
-
"https://litellm.example.com";
|
|
989
|
-
return `${normalizeBaseUrl(baseUrl, definition.allowInsecureHttp)}/v1`;
|
|
990
|
-
}
|
|
991
|
-
async function authForCredential(definition, credential, executeHelpers = true) {
|
|
992
|
-
if (credential?.type === "oauth") {
|
|
993
|
-
const baseUrl = typeof credential.baseUrl === "string"
|
|
994
|
-
? normalizeBaseUrl(credential.baseUrl, definition.allowInsecureHttp)
|
|
995
|
-
: normalizeBaseUrl(requestBaseUrl(definition), definition.allowInsecureHttp);
|
|
996
|
-
return {
|
|
997
|
-
baseUrl,
|
|
998
|
-
apiKey: credential.access,
|
|
999
|
-
headers: resolveHeaders(definition),
|
|
1000
|
-
allowInsecureHttp: definition.allowInsecureHttp,
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential, executeHelpers);
|
|
1004
|
-
if (!resolved?.auth.apiKey) {
|
|
1005
|
-
throw new Error(`no credentials for ${definition.name}. Run /login litellm or set env vars.`);
|
|
1006
|
-
}
|
|
1007
|
-
return {
|
|
1008
|
-
baseUrl: normalizeBaseUrl(resolved.auth.baseUrl ?? requestBaseUrl(definition), definition.allowInsecureHttp),
|
|
1009
|
-
apiKey: resolved.auth.apiKey,
|
|
1010
|
-
headers: resolved.auth.headers,
|
|
1011
|
-
allowInsecureHttp: definition.allowInsecureHttp,
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
let registeredMcpIdentity;
|
|
1015
|
-
let mcpRegistration;
|
|
1016
|
-
let defaultRuntimeAuth;
|
|
1017
|
-
function mcpCatalogIdentity(auth) {
|
|
1018
|
-
return JSON.stringify({
|
|
1019
|
-
baseUrl: normalizeBaseUrl(auth.baseUrl, auth.allowInsecureHttp),
|
|
1020
|
-
apiKey: auth.apiKey,
|
|
1021
|
-
headers: Object.entries(auth.headers ?? {}).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
async function getRuntimeAuth(ctx) {
|
|
1025
|
-
const auth = await ctx.modelRegistry.getProviderAuth(PROVIDER_NAME);
|
|
1026
|
-
if (!auth)
|
|
1027
|
-
return undefined;
|
|
1028
|
-
const provider = ctx.modelRegistry.getProvider(PROVIDER_NAME);
|
|
1029
|
-
const baseUrl = auth.auth.baseUrl ?? provider?.baseUrl;
|
|
1030
|
-
const apiKey = auth.auth.apiKey;
|
|
1031
|
-
if (!baseUrl || !apiKey)
|
|
1032
|
-
return undefined;
|
|
1033
|
-
const headers = Object.fromEntries(Object.entries(auth.auth.headers ?? provider?.headers ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
1034
|
-
return {
|
|
1035
|
-
baseUrl: normalizeBaseUrl(baseUrl, definitions[0]?.allowInsecureHttp),
|
|
1036
|
-
apiKey,
|
|
1037
|
-
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
1038
|
-
allowInsecureHttp: definitions[0]?.allowInsecureHttp,
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
async function resolveDefaultRuntimeAuth(ctx) {
|
|
1042
|
-
if (ctx?.modelRegistry) {
|
|
1043
|
-
const auth = await getRuntimeAuth(ctx);
|
|
1044
|
-
if (auth)
|
|
1045
|
-
return auth;
|
|
1046
|
-
throw new Error("no credentials for litellm. Run /login litellm or set env vars.");
|
|
1047
|
-
}
|
|
1048
|
-
if (!defaultRuntimeAuth)
|
|
1049
|
-
throw new Error("no credentials for litellm. Run /login litellm or set env vars.");
|
|
1050
|
-
return defaultRuntimeAuth;
|
|
1051
|
-
}
|
|
1052
|
-
function waitForMcpRegistration(registration, signal) {
|
|
1053
|
-
if (!signal)
|
|
1054
|
-
return registration.catch(() => undefined);
|
|
1055
|
-
signal.throwIfAborted();
|
|
1056
|
-
return new Promise((resolve, reject) => {
|
|
1057
|
-
const onAbort = () => {
|
|
1058
|
-
signal.removeEventListener("abort", onAbort);
|
|
1059
|
-
reject(signal.reason);
|
|
1060
|
-
};
|
|
1061
|
-
const onComplete = () => {
|
|
1062
|
-
signal.removeEventListener("abort", onAbort);
|
|
1063
|
-
resolve();
|
|
1064
|
-
};
|
|
1065
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1066
|
-
if (signal.aborted)
|
|
1067
|
-
onAbort();
|
|
1068
|
-
else
|
|
1069
|
-
registration.then(onComplete, onComplete);
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
async function registerMcpTools(auth, signal) {
|
|
1073
|
-
if (!mcpEnabled || discoveryDisabledReason())
|
|
1074
|
-
return;
|
|
1075
|
-
const identity = mcpCatalogIdentity(auth);
|
|
1076
|
-
while (mcpRegistration) {
|
|
1077
|
-
await waitForMcpRegistration(mcpRegistration, signal);
|
|
1078
|
-
signal?.throwIfAborted();
|
|
1079
|
-
if (registeredMcpIdentity === identity)
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
if (registeredMcpIdentity === identity)
|
|
1083
|
-
return;
|
|
1084
|
-
const registration = (async () => {
|
|
1085
|
-
try {
|
|
1086
|
-
signal?.throwIfAborted();
|
|
1087
|
-
const tools = await createMcpToolDefinitions((ctx) => (ctx?.modelRegistry ? resolveDefaultRuntimeAuth(ctx) : Promise.resolve(auth)), isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM MCP: ${message}\n`) : undefined, signal);
|
|
1088
|
-
signal?.throwIfAborted();
|
|
1089
|
-
for (const tool of tools)
|
|
1090
|
-
pi.registerTool(tool);
|
|
1091
|
-
registeredMcpIdentity = identity;
|
|
1092
|
-
}
|
|
1093
|
-
catch (error) {
|
|
1094
|
-
if (signal?.aborted)
|
|
1095
|
-
throw signal.reason;
|
|
1096
|
-
process.stderr.write(`LiteLLM (${PROVIDER_NAME}): MCP tool discovery failed (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
1097
|
-
}
|
|
1098
|
-
})();
|
|
1099
|
-
mcpRegistration = registration;
|
|
1100
|
-
try {
|
|
1101
|
-
await registration;
|
|
1102
|
-
}
|
|
1103
|
-
finally {
|
|
1104
|
-
if (mcpRegistration === registration)
|
|
1105
|
-
mcpRegistration = undefined;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
async function runDiscovery(auth, signal) {
|
|
1109
|
-
const result = await discoverModels(auth.baseUrl, auth.apiKey, {
|
|
1110
|
-
timeoutMs: getDiscoveryTimeoutMs(),
|
|
1111
|
-
signal,
|
|
1112
|
-
headers: auth.headers,
|
|
1113
|
-
allowInsecureHttp: auth.allowInsecureHttp,
|
|
1114
|
-
silent: !isVerboseDiscovery(),
|
|
1115
|
-
onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
|
|
1116
|
-
});
|
|
1117
|
-
signal?.throwIfAborted();
|
|
1118
|
-
return { ...result, baseUrl: `${normalizeBaseUrl(auth.baseUrl, auth.allowInsecureHttp)}/v1` };
|
|
1119
|
-
}
|
|
1120
|
-
// Pi's startup path only ever refreshes with allowNetwork:false, and it returns before the
|
|
1121
|
-
// network phase, so the provider would stay empty until the user opens /model. Discover here
|
|
1122
|
-
// instead, the way 1.x did, and hand the catalog over as the provider's baseline models.
|
|
1123
|
-
async function seedModels(definition) {
|
|
1124
|
-
if (discoveryDisabledReason() || isHostOffline())
|
|
1125
|
-
return [];
|
|
1126
|
-
try {
|
|
1127
|
-
const stored = readStoredCredential(definition.name, join(getAgentDir(), "auth.json"));
|
|
1128
|
-
// executeHelpers:false — activation must never run the user's key helper as a side effect,
|
|
1129
|
-
// so helper-backed setups keep waiting for Pi's own refresh.
|
|
1130
|
-
const auth = await authForCredential(definition, stored, false);
|
|
1131
|
-
const result = await runDiscovery(auth, AbortSignal.timeout(getSeedTimeoutMs()));
|
|
1132
|
-
return toNativeModels(definition.name, result.baseUrl, result.models);
|
|
1133
|
-
}
|
|
1134
|
-
catch (error) {
|
|
1135
|
-
// No credentials yet is the normal unconfigured case; anything else is worth one line.
|
|
1136
|
-
if (isVerboseDiscovery()) {
|
|
1137
|
-
process.stderr.write(`LiteLLM (${definition.name}): startup discovery skipped (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
1138
|
-
}
|
|
1139
|
-
return [];
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
const seeded = await Promise.all(definitions.map(seedModels));
|
|
1143
|
-
for (const [index, definition] of definitions.entries()) {
|
|
1144
|
-
const provider = createLiteLLMProvider({
|
|
1145
|
-
id: definition.name,
|
|
1146
|
-
name: definition.displayName,
|
|
1147
|
-
baseUrl: requestBaseUrl(definition),
|
|
1148
|
-
auth: createProviderAuth(definition),
|
|
1149
|
-
models: seeded[index],
|
|
1150
|
-
discover: async (credential, signal) => {
|
|
1151
|
-
const disabledReason = discoveryDisabledReason();
|
|
1152
|
-
if (disabledReason)
|
|
1153
|
-
throw new Error(`discovery disabled (${disabledReason})`);
|
|
1154
|
-
return runDiscovery(await authForCredential(definition, credential), signal);
|
|
1155
|
-
},
|
|
1156
|
-
});
|
|
1157
|
-
Object.assign(provider, { headers: resolveHeaders(definition) });
|
|
1158
|
-
const refreshModels = provider.refreshModels;
|
|
1159
|
-
provider.refreshModels = async (context) => {
|
|
1160
|
-
try {
|
|
1161
|
-
await refreshModels(context);
|
|
1162
|
-
}
|
|
1163
|
-
finally {
|
|
1164
|
-
if (definition.name === PROVIDER_NAME &&
|
|
1165
|
-
context.allowNetwork &&
|
|
1166
|
-
!discoveryDisabledReason() &&
|
|
1167
|
-
context.credential) {
|
|
1168
|
-
const auth = await authForCredential(definition, context.credential);
|
|
1169
|
-
defaultRuntimeAuth = auth;
|
|
1170
|
-
void registerMcpTools(auth, context.signal).catch(() => undefined);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
pi.registerProvider(provider);
|
|
1175
|
-
}
|
|
1176
|
-
setupLiteLLMCostTracking(pi, [...providerNames]);
|
|
1177
|
-
if (skillsEnabled) {
|
|
1178
|
-
for (const tool of createSkillToolDefinitions(resolveDefaultRuntimeAuth)) {
|
|
1179
|
-
pi.registerTool(tool);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
let sessionId;
|
|
1183
|
-
pi.on("session_start", (_event, ctx) => {
|
|
1184
|
-
sessionId = getSessionIdFromFile(ctx.sessionManager.getSessionFile());
|
|
1185
|
-
});
|
|
1186
|
-
pi.on("before_provider_request", (event, ctx) => {
|
|
1187
|
-
if (!ctx.model?.provider || !providerNames.has(ctx.model.provider))
|
|
1188
|
-
return;
|
|
1189
|
-
if (typeof event.payload !== "object" || event.payload === null)
|
|
1190
|
-
return;
|
|
1191
|
-
return prepareLiteLLMRequestPayload(event.payload, ctx.model, sessionId);
|
|
1192
|
-
});
|
|
1193
|
-
// Skills enrichment is best-effort: an expired credential or an unreachable proxy must not
|
|
1194
|
-
// report an extension error on every turn. The failing model request states the real problem.
|
|
1195
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
1196
|
-
defaultRuntimeAuth = undefined;
|
|
1197
|
-
if (!skillsEnabled || discoveryDisabledReason())
|
|
1198
|
-
return;
|
|
1199
|
-
let section;
|
|
1200
|
-
try {
|
|
1201
|
-
const auth = await getRuntimeAuth(ctx);
|
|
1202
|
-
if (!auth)
|
|
1203
|
-
return;
|
|
1204
|
-
defaultRuntimeAuth = auth;
|
|
1205
|
-
const skills = await listSkills(auth.baseUrl, auth.apiKey, auth.headers, auth.allowInsecureHttp);
|
|
1206
|
-
section = createSkillsPromptSection(skills);
|
|
1207
|
-
}
|
|
1208
|
-
catch (error) {
|
|
1209
|
-
if (isVerboseDiscovery()) {
|
|
1210
|
-
process.stderr.write(`LiteLLM: skipping Skills (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
1211
|
-
}
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
if (!section)
|
|
1215
|
-
return;
|
|
1216
|
-
return { systemPrompt: `${event.systemPrompt}\n\n${section}` };
|
|
1217
|
-
});
|
|
1218
|
-
pi.on("message_end", (event) => {
|
|
1219
|
-
if (event.message.role !== "assistant")
|
|
1220
|
-
return;
|
|
1221
|
-
const message = normalizeThinkTags(event.message, providerNames);
|
|
1222
|
-
if (!message)
|
|
1223
|
-
return;
|
|
1224
|
-
return { message };
|
|
1225
|
-
});
|
|
1226
|
-
}
|