pi-provider-litellm 1.2.9 → 2.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 +40 -23
- package/dist/cache.d.ts +1 -5
- package/dist/cache.js +3 -42
- package/dist/cost.d.ts +2 -12
- package/dist/cost.js +0 -0
- package/dist/discover.d.ts +6 -8
- package/dist/discover.js +222 -84
- package/dist/index.js +457 -643
- package/dist/litellm.js +2 -4
- package/dist/mcp-tools.d.ts +4 -4
- package/dist/mcp-tools.js +24 -30
- package/dist/provider.d.ts +13 -0
- package/dist/provider.js +30 -0
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +11 -11
- package/dist/types.d.ts +13 -10
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { fingerprint, readCache, writeCache } from "./cache.js";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { setupLiteLLMCostTracking } from "./cost.js";
|
|
7
|
-
import { discoverModels, isGpt55Model, normalizeBaseUrl, shouldSuppressReasoningContent
|
|
6
|
+
import { discoverModels, isGpt55Model, normalizeBaseUrl, shouldSuppressReasoningContent } from "./discover.js";
|
|
8
7
|
import { getGcloudToken, getGcloudTokenCacheKey, getGcloudTokenCommand, isGcloudTokenAuthEnabled, } from "./gcloud-token.js";
|
|
9
8
|
import { getSessionIdFromFile } from "./litellm.js";
|
|
10
9
|
import { createMcpToolDefinitions } from "./mcp-tools.js";
|
|
10
|
+
import { createLiteLLMProvider } from "./provider.js";
|
|
11
11
|
import { createSkillsPromptSection, createSkillToolDefinitions, listSkills } from "./skills.js";
|
|
12
12
|
const PROVIDER_NAME = "litellm";
|
|
13
13
|
const SETTINGS_KEY = "litellm";
|
|
@@ -17,159 +17,23 @@ const ENV_API_KEY_HELPER = "LITELLM_API_KEY_HELPER";
|
|
|
17
17
|
const ENV_HEADERS = "LITELLM_HEADERS";
|
|
18
18
|
const ENV_TIMEOUT = "LITELLM_DISCOVERY_TIMEOUT_MS";
|
|
19
19
|
const ENV_OFFLINE = "LITELLM_OFFLINE";
|
|
20
|
+
const ENV_VERBOSE_DISCOVERY = "LITELLM_VERBOSE_DISCOVERY";
|
|
21
|
+
const ENV_MODELS_DEV = "LITELLM_MODELS_DEV";
|
|
20
22
|
const DEFAULT_TIMEOUT_MS = 5000;
|
|
21
23
|
const LOGIN_TIMEOUT_MS = 10_000;
|
|
22
|
-
const
|
|
23
|
-
const CACHE_STALE_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
const MODELS_DEV_CACHE_FILENAME = "litellm-models-dev.json";
|
|
24
25
|
const TOKEN_REFRESH_LEAD_MS = 5 * 60 * 1000;
|
|
25
26
|
const PERMANENT_TOKEN_EXPIRES_AT = Number.MAX_SAFE_INTEGER;
|
|
26
27
|
const EXPIRE_TOKEN_IMMEDIATELY = 0;
|
|
27
|
-
function
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.toLowerCase()
|
|
33
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
34
|
-
.replace(/^-+|-+$/g, "") || "provider");
|
|
35
|
-
}
|
|
36
|
-
function getCachePath(providerName = PROVIDER_NAME) {
|
|
37
|
-
if (providerName === PROVIDER_NAME)
|
|
38
|
-
return join(getAgentDir(), CACHE_FILENAME);
|
|
39
|
-
return join(getAgentDir(), `litellm-models-${sanitizeCacheSegment(providerName)}.json`);
|
|
40
|
-
}
|
|
41
|
-
// Same tolerance as pi core's models.json loader (stripJsonComments in dist/utils/json.js):
|
|
42
|
-
// strip `//` line comments and trailing commas, leaving string literals untouched.
|
|
43
|
-
function stripJsonComments(input) {
|
|
44
|
-
return input
|
|
45
|
-
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
|
46
|
-
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? m);
|
|
28
|
+
function getModelsDevDiscoveryOptions() {
|
|
29
|
+
return {
|
|
30
|
+
modelsDev: process.env[ENV_MODELS_DEV] !== "0",
|
|
31
|
+
modelsDevCachePath: join(getAgentDir(), MODELS_DEV_CACHE_FILENAME),
|
|
32
|
+
};
|
|
47
33
|
}
|
|
48
34
|
function isPlainObject(value) {
|
|
49
35
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
50
36
|
}
|
|
51
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
52
|
-
// Mirrors pi core's ModelOverrideSchema: core rejects the whole models.json on invalid values,
|
|
53
|
-
// so anything dropped here would also have been flagged for built-in providers. Headers matter
|
|
54
|
-
// most: pi resolves each header value as a config string, so non-strings break requests.
|
|
55
|
-
const MODEL_OVERRIDE_VALIDATORS = {
|
|
56
|
-
name: (value) => typeof value === "string",
|
|
57
|
-
reasoning: (value) => typeof value === "boolean",
|
|
58
|
-
thinkingLevelMap: (value) => isPlainObject(value) && THINKING_LEVELS.every((level) => value[level] == null || typeof value[level] === "string"),
|
|
59
|
-
input: (value) => Array.isArray(value) && value.every((entry) => entry === "text" || entry === "image"),
|
|
60
|
-
contextWindow: (value) => typeof value === "number",
|
|
61
|
-
maxTokens: (value) => typeof value === "number",
|
|
62
|
-
headers: (value) => isPlainObject(value) && Object.values(value).every((entry) => typeof entry === "string"),
|
|
63
|
-
compat: isPlainObject,
|
|
64
|
-
cost: (value) => isPlainObject(value) && Object.values(value).every((entry) => typeof entry === "number"),
|
|
65
|
-
};
|
|
66
|
-
function sanitizeModelOverride(modelId, raw) {
|
|
67
|
-
if (!isPlainObject(raw)) {
|
|
68
|
-
process.stderr.write(`LiteLLM: ignoring model override for ${modelId} in models.json (not an object).\n`);
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
const override = {};
|
|
72
|
-
const dropped = [];
|
|
73
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
74
|
-
const isValid = MODEL_OVERRIDE_VALIDATORS[key];
|
|
75
|
-
if (isValid?.(value))
|
|
76
|
-
override[key] = value;
|
|
77
|
-
else
|
|
78
|
-
dropped.push(key);
|
|
79
|
-
}
|
|
80
|
-
if (dropped.length > 0) {
|
|
81
|
-
process.stderr.write(`LiteLLM: ignoring invalid model override field(s) for ${modelId} in models.json: ${dropped.join(", ")}.\n`);
|
|
82
|
-
}
|
|
83
|
-
return override;
|
|
84
|
-
}
|
|
85
|
-
// Overrides are read per registered provider name, matching pi core's own
|
|
86
|
-
// models.json layout (`providers.<name>.modelOverrides`) so aliases get the
|
|
87
|
-
// same override support as the default provider, not a special case of it.
|
|
88
|
-
async function readModelOverrides(providerName) {
|
|
89
|
-
let raw;
|
|
90
|
-
try {
|
|
91
|
-
raw = await readFile(join(getAgentDir(), "models.json"), "utf8");
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return new Map();
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
const config = JSON.parse(stripJsonComments(raw));
|
|
98
|
-
const overrides = new Map();
|
|
99
|
-
for (const [id, rawOverride] of Object.entries(config.providers?.[providerName]?.modelOverrides ?? {})) {
|
|
100
|
-
const override = sanitizeModelOverride(id, rawOverride);
|
|
101
|
-
if (override)
|
|
102
|
-
overrides.set(id, override);
|
|
103
|
-
}
|
|
104
|
-
return overrides;
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
-
process.stderr.write(`LiteLLM: ignoring model overrides (failed to parse models.json: ${message}).\n`);
|
|
109
|
-
return new Map();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// Merge semantics must match pi core's applyModelOverride/mergeCompat (dist/core/model-registry.js)
|
|
113
|
-
// so the same models.json entry behaves identically for litellm and built-in providers.
|
|
114
|
-
function mergeCompat(base, override) {
|
|
115
|
-
if (!override)
|
|
116
|
-
return base;
|
|
117
|
-
const merged = { ...base, ...override };
|
|
118
|
-
for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs"]) {
|
|
119
|
-
const baseValue = base?.[key];
|
|
120
|
-
const overrideValue = override[key];
|
|
121
|
-
if (baseValue || overrideValue) {
|
|
122
|
-
merged[key] = { ...baseValue, ...overrideValue };
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return merged;
|
|
126
|
-
}
|
|
127
|
-
function applyModelOverride(model, override) {
|
|
128
|
-
const next = { ...model };
|
|
129
|
-
if (override.name !== undefined)
|
|
130
|
-
next.name = override.name;
|
|
131
|
-
if (override.reasoning !== undefined)
|
|
132
|
-
next.reasoning = override.reasoning;
|
|
133
|
-
if (override.thinkingLevelMap !== undefined) {
|
|
134
|
-
next.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };
|
|
135
|
-
}
|
|
136
|
-
if (override.input !== undefined)
|
|
137
|
-
next.input = override.input;
|
|
138
|
-
if (override.contextWindow !== undefined)
|
|
139
|
-
next.contextWindow = override.contextWindow;
|
|
140
|
-
if (override.maxTokens !== undefined)
|
|
141
|
-
next.maxTokens = override.maxTokens;
|
|
142
|
-
if (override.headers !== undefined)
|
|
143
|
-
next.headers = override.headers;
|
|
144
|
-
if (override.compat !== undefined)
|
|
145
|
-
next.compat = mergeCompat(model.compat, override.compat);
|
|
146
|
-
if (override.cost !== undefined)
|
|
147
|
-
next.cost = { ...model.cost, ...override.cost };
|
|
148
|
-
return next;
|
|
149
|
-
}
|
|
150
|
-
function applyModelOverrides(models, overrides) {
|
|
151
|
-
if (overrides.size === 0)
|
|
152
|
-
return models;
|
|
153
|
-
return models.map((model) => {
|
|
154
|
-
const override = overrides.get(model.id);
|
|
155
|
-
return override ? applyModelOverride(model, override) : model;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
// Re-reads models.json on every call so overrides edited mid-session take effect on the next
|
|
159
|
-
// refresh or login, matching pi core's live reload for built-in providers.
|
|
160
|
-
async function applyOverrides(providerName, models) {
|
|
161
|
-
return applyModelOverrides(models, await readModelOverrides(providerName));
|
|
162
|
-
}
|
|
163
|
-
async function readAuthEntry(providerName = PROVIDER_NAME) {
|
|
164
|
-
try {
|
|
165
|
-
const raw = await readFile(getAuthPath(), "utf8");
|
|
166
|
-
const parsed = JSON.parse(raw);
|
|
167
|
-
return parsed?.[providerName];
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
return undefined;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
37
|
async function readGlobalLiteLLMSettings() {
|
|
174
38
|
try {
|
|
175
39
|
const raw = await readFile(join(getAgentDir(), "settings.json"), "utf8");
|
|
@@ -199,9 +63,50 @@ function normalizeCommand(raw) {
|
|
|
199
63
|
function getApiKeyHelperCommand() {
|
|
200
64
|
return normalizeCommand(process.env[ENV_API_KEY_HELPER]);
|
|
201
65
|
}
|
|
202
|
-
function
|
|
66
|
+
function parseApiKeyCommand(commandConfig) {
|
|
203
67
|
const command = commandConfig.startsWith("!") ? commandConfig.slice(1) : commandConfig;
|
|
204
|
-
|
|
68
|
+
if (/[\n\r;&|<>`$(){}*?~%^!]|\[|\]/.test(command))
|
|
69
|
+
throw new Error("LiteLLM API key helper shell syntax is not supported");
|
|
70
|
+
const parts = [];
|
|
71
|
+
let current = "";
|
|
72
|
+
let quote;
|
|
73
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
74
|
+
const character = command[index];
|
|
75
|
+
const next = command[index + 1];
|
|
76
|
+
if (character === "\\" && quote !== "'" && next && (next === "\\" || next === quote || /\s/.test(next))) {
|
|
77
|
+
current += next;
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
else if ((character === "'" || character === '"') && (!quote || quote === character)) {
|
|
81
|
+
quote = quote ? undefined : character;
|
|
82
|
+
}
|
|
83
|
+
else if (!quote && /\s/.test(character)) {
|
|
84
|
+
if (current) {
|
|
85
|
+
parts.push(current);
|
|
86
|
+
current = "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
current += character;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (quote)
|
|
94
|
+
throw new Error(`LiteLLM API key helper command has an unterminated quote: ${command}`);
|
|
95
|
+
if (current)
|
|
96
|
+
parts.push(current);
|
|
97
|
+
if (parts.length === 0)
|
|
98
|
+
throw new Error("LiteLLM API key helper command is empty");
|
|
99
|
+
if (/\.(?:cmd|bat)$/i.test(parts[0]))
|
|
100
|
+
throw new Error("LiteLLM API key helper shell scripts are not supported; use an executable");
|
|
101
|
+
return parts;
|
|
102
|
+
}
|
|
103
|
+
function executeApiKeyCommand(commandConfig) {
|
|
104
|
+
const [command, ...args] = parseApiKeyCommand(commandConfig);
|
|
105
|
+
const output = execFileSync(command, args, {
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
108
|
+
timeout: 10_000,
|
|
109
|
+
}).trim();
|
|
205
110
|
if (!output)
|
|
206
111
|
throw new Error(`LiteLLM API key helper produced no output: ${command}`);
|
|
207
112
|
return output;
|
|
@@ -220,53 +125,29 @@ function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
|
|
|
220
125
|
return opaqueFallback;
|
|
221
126
|
}
|
|
222
127
|
}
|
|
223
|
-
function openInBrowser(url) {
|
|
224
|
-
// Never invoke a shell here: cmd.exe re-parses metacharacters before `start`
|
|
225
|
-
// runs, which would make URL contents injectable. Launch is best-effort; the
|
|
226
|
-
// URL is also shown to the user, so launcher failures must not crash the process.
|
|
227
|
-
const [cmd, args] = process.platform === "darwin"
|
|
228
|
-
? ["open", [url]]
|
|
229
|
-
: process.platform === "win32"
|
|
230
|
-
? ["rundll32", ["url.dll,FileProtocolHandler", url]]
|
|
231
|
-
: ["xdg-open", [url]];
|
|
232
|
-
spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
233
|
-
.on("error", () => undefined)
|
|
234
|
-
.unref();
|
|
235
|
-
}
|
|
236
128
|
async function generateVirtualKey(baseUrl, userToken, signal, headers) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
const data = (await response.json());
|
|
254
|
-
if (typeof data.key !== "string" || !data.key)
|
|
255
|
-
throw new Error("No key in response from /key/generate");
|
|
256
|
-
const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
|
|
257
|
-
return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
|
|
258
|
-
}
|
|
259
|
-
finally {
|
|
260
|
-
cancel();
|
|
129
|
+
const boundedSignal = signal
|
|
130
|
+
? AbortSignal.any([signal, AbortSignal.timeout(LOGIN_TIMEOUT_MS)])
|
|
131
|
+
: AbortSignal.timeout(LOGIN_TIMEOUT_MS);
|
|
132
|
+
const response = await fetch(`${baseUrl}/key/generate`, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: {
|
|
135
|
+
...headers,
|
|
136
|
+
Authorization: `Bearer ${userToken}`,
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
},
|
|
139
|
+
body: JSON.stringify({}),
|
|
140
|
+
signal: boundedSignal,
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const text = await response.text().catch(() => "");
|
|
144
|
+
throw new Error(`Virtual key generation failed (${response.status}): ${text}`);
|
|
261
145
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return credentials.refresh.startsWith("!")
|
|
268
|
-
? executeApiKeyCommand(credentials.refresh)
|
|
269
|
-
: getLiteLLMApiKey(credentials);
|
|
146
|
+
const data = (await response.json());
|
|
147
|
+
if (typeof data.key !== "string" || !data.key)
|
|
148
|
+
throw new Error("No key in response from /key/generate");
|
|
149
|
+
const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
|
|
150
|
+
return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
|
|
270
151
|
}
|
|
271
152
|
function resolveTemplateConfigValue(config) {
|
|
272
153
|
let resolved = "";
|
|
@@ -310,6 +191,47 @@ function resolveTemplateConfigValue(config) {
|
|
|
310
191
|
}
|
|
311
192
|
return resolved;
|
|
312
193
|
}
|
|
194
|
+
async function resolveTemplateConfigValueFromContext(config, env) {
|
|
195
|
+
let resolved = "";
|
|
196
|
+
for (let index = 0; index < config.length;) {
|
|
197
|
+
const dollarIndex = config.indexOf("$", index);
|
|
198
|
+
if (dollarIndex === -1)
|
|
199
|
+
return resolved + config.slice(index);
|
|
200
|
+
resolved += config.slice(index, dollarIndex);
|
|
201
|
+
const nextChar = config[dollarIndex + 1];
|
|
202
|
+
if (nextChar === "$" || nextChar === "!") {
|
|
203
|
+
resolved += nextChar;
|
|
204
|
+
index = dollarIndex + 2;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (nextChar === "{") {
|
|
208
|
+
const endIndex = config.indexOf("}", dollarIndex + 2);
|
|
209
|
+
if (endIndex === -1) {
|
|
210
|
+
resolved += "$";
|
|
211
|
+
index = dollarIndex + 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const envValue = await env(config.slice(dollarIndex + 2, endIndex));
|
|
215
|
+
if (envValue === undefined)
|
|
216
|
+
return undefined;
|
|
217
|
+
resolved += envValue;
|
|
218
|
+
index = endIndex + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const match = config.slice(dollarIndex + 1).match(/^[A-Za-z_][A-Za-z0-9_]*/);
|
|
222
|
+
if (!match) {
|
|
223
|
+
resolved += "$";
|
|
224
|
+
index = dollarIndex + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const envValue = await env(match[0]);
|
|
228
|
+
if (envValue === undefined)
|
|
229
|
+
return undefined;
|
|
230
|
+
resolved += envValue;
|
|
231
|
+
index = dollarIndex + 1 + match[0].length;
|
|
232
|
+
}
|
|
233
|
+
return resolved;
|
|
234
|
+
}
|
|
313
235
|
function resolveConfigValue(config, { executeCommands }) {
|
|
314
236
|
if (config.startsWith("!"))
|
|
315
237
|
return executeCommands ? executeApiKeyCommand(config) : undefined;
|
|
@@ -361,97 +283,56 @@ function resolveHeaders(definition) {
|
|
|
361
283
|
return parseCustomHeaders(resolveTemplateConfigValue(definition.headers));
|
|
362
284
|
return parseHeaderRecord(definition.headers);
|
|
363
285
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (!headers)
|
|
369
|
-
return undefined;
|
|
370
|
-
const sorted = Object.keys(headers)
|
|
371
|
-
.sort()
|
|
372
|
-
.map((key) => [key, headers[key]]);
|
|
373
|
-
return fingerprint(JSON.stringify(sorted));
|
|
374
|
-
}
|
|
375
|
-
// Pi core resolves registered header values with the same $VAR/!command syntax
|
|
376
|
-
// at request time, so already-resolved literals must be escaped or they get
|
|
377
|
-
// resolved a second time ($UNSET would then fail every request).
|
|
378
|
-
function escapeConfigValue(value) {
|
|
379
|
-
const escaped = value.replace(/\$/g, "$$$$");
|
|
380
|
-
return escaped.startsWith("!") ? `$${escaped}` : escaped;
|
|
381
|
-
}
|
|
382
|
-
function escapeHeaderConfig(headers) {
|
|
383
|
-
if (!headers)
|
|
384
|
-
return undefined;
|
|
385
|
-
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, escapeConfigValue(value)]));
|
|
286
|
+
async function resolveHeadersFromContext(definition, env) {
|
|
287
|
+
if (typeof definition.headers === "string")
|
|
288
|
+
return parseCustomHeaders(await resolveTemplateConfigValueFromContext(definition.headers, env));
|
|
289
|
+
return parseHeaderRecord(definition.headers);
|
|
386
290
|
}
|
|
387
291
|
async function resolveCredentials(definition, { executeHelpers = true } = {}) {
|
|
388
|
-
const entry = definition.useSavedAuth ? await readAuthEntry(definition.name) : undefined;
|
|
389
292
|
const configuredBase = cleanConfig(definition.baseUrl) ?? (definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined);
|
|
390
293
|
const envKey = definition.useDefaultEnv ? cleanConfig(process.env[ENV_API_KEY]) : undefined;
|
|
391
294
|
const envHelperCommand = definition.useDefaultEnv ? getApiKeyHelperCommand() : undefined;
|
|
392
295
|
const useGcloudToken = definition.useGcloudTokenAuth && isGcloudTokenAuthEnabled();
|
|
393
|
-
const
|
|
394
|
-
const gcloudCacheKey = useGcloudToken && !entry ? ((await getGcloudTokenCacheKey()) ?? undefined) : undefined;
|
|
395
|
-
const authKey = entry?.type === "oauth"
|
|
396
|
-
? (executeHelpers ? resolveOAuthApiKey(entry) : entry.access).trim()
|
|
397
|
-
: entry?.type === "api_key"
|
|
398
|
-
? (await AuthStorage.create(getAuthPath()).getApiKey(definition.name, { includeFallback: false }))?.trim()
|
|
399
|
-
: undefined;
|
|
296
|
+
const gcloudCacheKey = useGcloudToken ? ((await getGcloudTokenCacheKey()) ?? undefined) : undefined;
|
|
400
297
|
const gcloudKey = executeHelpers && gcloudCacheKey ? (await getGcloudToken())?.trim() : undefined;
|
|
401
298
|
// Resolved lazily so a `!command` key is not executed when a
|
|
402
299
|
// higher-precedence credential (saved auth, gcloud token) already won.
|
|
403
300
|
let configuredKey;
|
|
404
|
-
if (!
|
|
301
|
+
if (!gcloudKey && definition.apiKeyConfig) {
|
|
405
302
|
configuredKey = resolveConfigValue(definition.apiKeyConfig, { executeCommands: executeHelpers });
|
|
406
303
|
if (configuredKey === undefined && !definition.apiKeyConfig.startsWith("!")) {
|
|
407
304
|
warnUnresolvedApiKeyConfig(definition.name, definition.apiKeyConfig);
|
|
408
305
|
}
|
|
409
306
|
}
|
|
410
|
-
const helperKey = !
|
|
307
|
+
const helperKey = !gcloudKey && !configuredKey && executeHelpers && envHelperCommand
|
|
411
308
|
? executeApiKeyCommand(envHelperCommand)
|
|
412
309
|
: undefined;
|
|
413
|
-
const apiKey =
|
|
414
|
-
let apiKeyFingerprint;
|
|
310
|
+
const apiKey = gcloudKey || configuredKey || helperKey || envKey;
|
|
415
311
|
let apiKeyConfig;
|
|
416
|
-
if (
|
|
417
|
-
apiKeyFingerprint = fingerprint(entry.refresh);
|
|
418
|
-
}
|
|
419
|
-
else if (authKey) {
|
|
420
|
-
apiKeyFingerprint = fingerprint(authKey);
|
|
421
|
-
}
|
|
422
|
-
else if (gcloudKey) {
|
|
423
|
-
apiKeyFingerprint = fingerprint(gcloudCacheKey ?? gcloudKey);
|
|
312
|
+
if (gcloudKey) {
|
|
424
313
|
apiKeyConfig = getGcloudTokenCommand();
|
|
425
314
|
}
|
|
426
315
|
else if (!executeHelpers && gcloudCacheKey) {
|
|
427
|
-
apiKeyFingerprint = fingerprint(gcloudCacheKey);
|
|
428
316
|
apiKeyConfig = getGcloudTokenCommand();
|
|
429
317
|
}
|
|
430
318
|
else if (configuredKey && definition.apiKeyConfig) {
|
|
431
|
-
apiKeyFingerprint = fingerprint(definition.apiKeyConfig.startsWith("!") ? definition.apiKeyConfig : configuredKey);
|
|
432
319
|
apiKeyConfig = definition.apiKeyConfig;
|
|
433
320
|
}
|
|
434
321
|
else if (!executeHelpers && definition.apiKeyConfig?.startsWith("!")) {
|
|
435
|
-
apiKeyFingerprint = fingerprint(definition.apiKeyConfig);
|
|
436
322
|
apiKeyConfig = definition.apiKeyConfig;
|
|
437
323
|
}
|
|
438
324
|
else if (helperKey && envHelperCommand) {
|
|
439
|
-
apiKeyFingerprint = fingerprint(envHelperCommand);
|
|
440
325
|
apiKeyConfig = envHelperCommand;
|
|
441
326
|
}
|
|
442
327
|
else if (!executeHelpers && envHelperCommand) {
|
|
443
|
-
apiKeyFingerprint = fingerprint(envHelperCommand);
|
|
444
328
|
apiKeyConfig = envHelperCommand;
|
|
445
329
|
}
|
|
446
330
|
else if (envKey) {
|
|
447
|
-
apiKeyFingerprint = fingerprint(envKey);
|
|
448
331
|
apiKeyConfig = `$${ENV_API_KEY}`;
|
|
449
332
|
}
|
|
450
|
-
const rawBase = authBase || configuredBase;
|
|
451
333
|
return {
|
|
452
|
-
baseUrl:
|
|
334
|
+
baseUrl: configuredBase ? normalizeBaseUrl(configuredBase) : undefined,
|
|
453
335
|
apiKey: apiKey || undefined,
|
|
454
|
-
apiKeyFingerprint,
|
|
455
336
|
apiKeyConfig,
|
|
456
337
|
};
|
|
457
338
|
}
|
|
@@ -467,8 +348,8 @@ function getDiscoveryTimeoutMs() {
|
|
|
467
348
|
function isOffline() {
|
|
468
349
|
return process.env[ENV_OFFLINE] === "1";
|
|
469
350
|
}
|
|
470
|
-
function
|
|
471
|
-
return process.
|
|
351
|
+
function isVerboseDiscovery() {
|
|
352
|
+
return process.env[ENV_VERBOSE_DISCOVERY] === "1";
|
|
472
353
|
}
|
|
473
354
|
function normalizeProviderSettings(raw) {
|
|
474
355
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -478,8 +359,11 @@ function normalizeProviderSettings(raw) {
|
|
|
478
359
|
return undefined;
|
|
479
360
|
return record;
|
|
480
361
|
}
|
|
481
|
-
|
|
482
|
-
const
|
|
362
|
+
function isFeatureEnabled(settings, feature) {
|
|
363
|
+
const raw = settings?.[feature];
|
|
364
|
+
return !isPlainObject(raw) || raw.enabled !== false;
|
|
365
|
+
}
|
|
366
|
+
function getProviderDefinitions(settings) {
|
|
483
367
|
const rawProviders = settings?.providers && typeof settings.providers === "object" ? settings.providers : undefined;
|
|
484
368
|
const providerSettings = rawProviders;
|
|
485
369
|
const defaultSettings = normalizeProviderSettings(providerSettings?.[PROVIDER_NAME]);
|
|
@@ -491,128 +375,81 @@ async function getProviderDefinitions() {
|
|
|
491
375
|
headers: raw?.headers ?? (isDefault ? `$${ENV_HEADERS}` : undefined),
|
|
492
376
|
useDefaultEnv: isDefault,
|
|
493
377
|
useGcloudTokenAuth: isDefault,
|
|
494
|
-
useSavedAuth: true,
|
|
495
378
|
enableOAuth: isDefault,
|
|
496
379
|
});
|
|
497
380
|
const definitions = [makeDefinition(PROVIDER_NAME, defaultSettings, true)];
|
|
498
|
-
const usedCacheSegments = new Map();
|
|
499
381
|
for (const [name, raw] of Object.entries(providerSettings ?? {})) {
|
|
500
382
|
if (name === PROVIDER_NAME)
|
|
501
383
|
continue;
|
|
502
384
|
const normalized = normalizeProviderSettings(raw);
|
|
503
385
|
if (!normalized)
|
|
504
386
|
continue;
|
|
505
|
-
// Distinct alias names can sanitize to the same cache file; registering
|
|
506
|
-
// both would let their model caches silently clobber each other.
|
|
507
|
-
const segment = sanitizeCacheSegment(name);
|
|
508
|
-
const existing = usedCacheSegments.get(segment);
|
|
509
|
-
if (existing) {
|
|
510
|
-
process.stderr.write(`LiteLLM: provider alias "${name}" would share a cache file with "${existing}"; skipping it. Rename the alias.\n`);
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
usedCacheSegments.set(segment, name);
|
|
514
387
|
definitions.push(makeDefinition(name, normalized, false));
|
|
515
388
|
}
|
|
516
389
|
return definitions;
|
|
517
390
|
}
|
|
518
|
-
async function
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
catch (error) {
|
|
523
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
524
|
-
return {
|
|
525
|
-
result: { models: [], source: "models_list" },
|
|
526
|
-
warning: message,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
async function loginLiteLLM(callbacks, options) {
|
|
531
|
-
const rawBaseUrl = (await callbacks.onPrompt({
|
|
391
|
+
async function loginApiKey(interaction) {
|
|
392
|
+
const rawBaseUrl = (await interaction.prompt({
|
|
393
|
+
type: "text",
|
|
532
394
|
message: "Enter LiteLLM proxy URL (no trailing /v1):",
|
|
533
395
|
placeholder: "https://litellm.example.com",
|
|
534
396
|
})).trim();
|
|
535
397
|
if (!rawBaseUrl)
|
|
536
398
|
throw new Error("Base URL is required");
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
399
|
+
const key = (await interaction.prompt({ type: "secret", message: "Enter API key:" })).trim();
|
|
400
|
+
if (!key)
|
|
401
|
+
throw new Error("Both base URL and API key are required");
|
|
402
|
+
return { type: "api_key", key, env: { [ENV_BASE_URL]: normalizeBaseUrl(rawBaseUrl) } };
|
|
403
|
+
}
|
|
404
|
+
async function loginOAuth(interaction, headers) {
|
|
405
|
+
const rawBaseUrl = (await interaction.prompt({
|
|
406
|
+
type: "text",
|
|
407
|
+
message: "Enter LiteLLM proxy URL (no trailing /v1):",
|
|
408
|
+
placeholder: "https://litellm.example.com",
|
|
540
409
|
})).trim();
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
catch (error) {
|
|
571
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
572
|
-
callbacks.onProgress?.(`LiteLLM: virtual key generation failed (${message}); using SSO token directly.`);
|
|
573
|
-
apiKey = rawToken;
|
|
574
|
-
refresh = "";
|
|
575
|
-
expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
576
|
-
}
|
|
410
|
+
if (!rawBaseUrl)
|
|
411
|
+
throw new Error("Base URL is required");
|
|
412
|
+
const baseUrl = normalizeBaseUrl(rawBaseUrl);
|
|
413
|
+
interaction.notify({
|
|
414
|
+
type: "auth_url",
|
|
415
|
+
url: `${baseUrl}/sso/key/generate`,
|
|
416
|
+
instructions: "Authenticate via SSO, then copy your token from the LiteLLM UI.",
|
|
417
|
+
});
|
|
418
|
+
const rawToken = (await interaction.prompt({ type: "secret", message: "Paste your SSO token from the LiteLLM UI:" }))
|
|
419
|
+
.trim()
|
|
420
|
+
.replace(/^Bearer\s+/i, "")
|
|
421
|
+
.trim();
|
|
422
|
+
if (!rawToken)
|
|
423
|
+
throw new Error("SSO token is required");
|
|
424
|
+
const wantVirtualKey = (await interaction.prompt({ type: "text", message: "Generate a LiteLLM virtual key from this token? (y/n):" }))
|
|
425
|
+
.trim()
|
|
426
|
+
.toLowerCase();
|
|
427
|
+
let access = rawToken;
|
|
428
|
+
let expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
429
|
+
if (wantVirtualKey !== "n" && wantVirtualKey !== "no") {
|
|
430
|
+
try {
|
|
431
|
+
interaction.notify({ type: "progress", message: "Generating virtual key..." });
|
|
432
|
+
const generated = await generateVirtualKey(baseUrl, rawToken, interaction.signal, headers);
|
|
433
|
+
access = generated.key;
|
|
434
|
+
expires =
|
|
435
|
+
generated.expiresAt === undefined
|
|
436
|
+
? PERMANENT_TOKEN_EXPIRES_AT
|
|
437
|
+
: Math.max(Date.now(), generated.expiresAt - TOKEN_REFRESH_LEAD_MS);
|
|
438
|
+
interaction.notify({ type: "progress", message: "Virtual key generated and will be used for API calls." });
|
|
577
439
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
440
|
+
catch (error) {
|
|
441
|
+
if (interaction.signal?.aborted)
|
|
442
|
+
throw interaction.signal.reason;
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
interaction.notify({
|
|
445
|
+
type: "progress",
|
|
446
|
+
message: `LiteLLM: virtual key generation failed (${message}); using SSO token directly.`,
|
|
447
|
+
});
|
|
582
448
|
}
|
|
583
449
|
}
|
|
584
|
-
|
|
585
|
-
const apiKeyInput = (await callbacks.onPrompt({ message: "Enter API key:" })).trim();
|
|
586
|
-
if (!apiKeyInput)
|
|
587
|
-
throw new Error("Both base URL and API key are required");
|
|
588
|
-
refresh = apiKeyInput.startsWith("!") ? apiKeyInput : "";
|
|
589
|
-
apiKey = refresh ? executeApiKeyCommand(refresh) : apiKeyInput;
|
|
590
|
-
expires = tokenExpiresAt(apiKey, refresh ? EXPIRE_TOKEN_IMMEDIATELY : PERMANENT_TOKEN_EXPIRES_AT);
|
|
591
|
-
}
|
|
592
|
-
const { models, source } = await discoverModels(baseUrl, apiKey, {
|
|
593
|
-
timeoutMs: LOGIN_TIMEOUT_MS,
|
|
594
|
-
signal: callbacks.signal,
|
|
595
|
-
headers: options.headers,
|
|
596
|
-
});
|
|
597
|
-
const cache = {
|
|
598
|
-
baseUrl,
|
|
599
|
-
apiKeyFingerprint: fingerprint(refresh || apiKey),
|
|
600
|
-
headersFingerprint: computeHeadersFingerprint(options.headers),
|
|
601
|
-
fetchedAt: Date.now(),
|
|
602
|
-
source,
|
|
603
|
-
models,
|
|
604
|
-
};
|
|
605
|
-
await writeCache(options.cachePath, cache);
|
|
606
|
-
await options.onCacheWrite?.(cache);
|
|
607
|
-
callbacks.onProgress?.(`LiteLLM: ${models.length} models discovered (source: ${source})`);
|
|
608
|
-
return {
|
|
609
|
-
access: apiKey,
|
|
610
|
-
refresh,
|
|
611
|
-
expires,
|
|
612
|
-
baseUrl,
|
|
613
|
-
};
|
|
450
|
+
return { type: "oauth", access, refresh: "", expires, baseUrl };
|
|
614
451
|
}
|
|
615
|
-
async function refreshLiteLLM(credentials) {
|
|
452
|
+
async function refreshLiteLLM(credentials, _signal) {
|
|
616
453
|
if (!credentials.refresh.startsWith("!")) {
|
|
617
454
|
if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
|
|
618
455
|
throw new Error("LiteLLM credential cannot be refreshed; run /login litellm again");
|
|
@@ -622,11 +459,122 @@ async function refreshLiteLLM(credentials) {
|
|
|
622
459
|
const access = executeApiKeyCommand(credentials.refresh);
|
|
623
460
|
return { ...credentials, access, expires: tokenExpiresAt(access, EXPIRE_TOKEN_IMMEDIATELY) };
|
|
624
461
|
}
|
|
625
|
-
function
|
|
626
|
-
const baseUrl =
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
462
|
+
async function resolveApiKeyAuth(definition, ctx, credential, executeHelpers = true) {
|
|
463
|
+
const baseUrl = cleanConfig(credential?.env?.[ENV_BASE_URL]) ??
|
|
464
|
+
cleanConfig(definition.baseUrl) ??
|
|
465
|
+
(definition.useDefaultEnv ? cleanConfig(await ctx.env(ENV_BASE_URL)) : undefined);
|
|
466
|
+
const stored = credential?.key
|
|
467
|
+
? resolveConfigValue(credential.key, { executeCommands: executeHelpers })?.trim()
|
|
468
|
+
: undefined;
|
|
469
|
+
let source;
|
|
470
|
+
let creds;
|
|
471
|
+
if (stored) {
|
|
472
|
+
source = "stored credential";
|
|
473
|
+
creds = {
|
|
474
|
+
baseUrl: baseUrl ? normalizeBaseUrl(baseUrl) : undefined,
|
|
475
|
+
apiKey: stored,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
creds = await resolveCredentials({ ...definition, apiKeyConfig: undefined, useDefaultEnv: false }, { executeHelpers });
|
|
480
|
+
if (!creds.apiKey && definition.apiKeyConfig) {
|
|
481
|
+
const configured = definition.apiKeyConfig.startsWith("!")
|
|
482
|
+
? executeHelpers
|
|
483
|
+
? executeApiKeyCommand(definition.apiKeyConfig)
|
|
484
|
+
: undefined
|
|
485
|
+
: await resolveTemplateConfigValueFromContext(definition.apiKeyConfig, ctx.env);
|
|
486
|
+
if (configured) {
|
|
487
|
+
creds.apiKey = configured;
|
|
488
|
+
creds.apiKeyConfig = definition.apiKeyConfig;
|
|
489
|
+
}
|
|
490
|
+
else if (!definition.apiKeyConfig.startsWith("!")) {
|
|
491
|
+
warnUnresolvedApiKeyConfig(definition.name, definition.apiKeyConfig);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (!creds.apiKey && definition.useDefaultEnv) {
|
|
495
|
+
const helper = normalizeCommand(await ctx.env(ENV_API_KEY_HELPER));
|
|
496
|
+
if (helper) {
|
|
497
|
+
creds.apiKey = executeHelpers ? executeApiKeyCommand(helper) : undefined;
|
|
498
|
+
creds.apiKeyConfig = helper;
|
|
499
|
+
source = ENV_API_KEY_HELPER;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const envKey = cleanConfig(await ctx.env(ENV_API_KEY));
|
|
503
|
+
if (envKey) {
|
|
504
|
+
creds.apiKey = envKey;
|
|
505
|
+
creds.apiKeyConfig = ENV_API_KEY;
|
|
506
|
+
source = ENV_API_KEY;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!creds.baseUrl && baseUrl)
|
|
511
|
+
creds.baseUrl = normalizeBaseUrl(baseUrl);
|
|
512
|
+
}
|
|
513
|
+
if (!creds.apiKey)
|
|
514
|
+
return undefined;
|
|
515
|
+
return {
|
|
516
|
+
auth: {
|
|
517
|
+
apiKey: creds.apiKey,
|
|
518
|
+
baseUrl: creds.baseUrl ? `${creds.baseUrl}/v1` : undefined,
|
|
519
|
+
headers: await resolveHeadersFromContext(definition, ctx.env),
|
|
520
|
+
},
|
|
521
|
+
env: baseUrl ? { [ENV_BASE_URL]: normalizeBaseUrl(baseUrl) } : undefined,
|
|
522
|
+
source: source ?? creds.apiKeyConfig ?? ENV_API_KEY,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function createProviderAuth(definition) {
|
|
526
|
+
return {
|
|
527
|
+
apiKey: {
|
|
528
|
+
name: `${definition.displayName} API key`,
|
|
529
|
+
login: definition.name === PROVIDER_NAME ? loginApiKey : undefined,
|
|
530
|
+
check: async ({ ctx, credential }) => {
|
|
531
|
+
const baseUrl = credential?.env?.[ENV_BASE_URL] ??
|
|
532
|
+
definition.baseUrl ??
|
|
533
|
+
(definition.useDefaultEnv ? await ctx.env(ENV_BASE_URL) : undefined);
|
|
534
|
+
if (!cleanConfig(baseUrl))
|
|
535
|
+
return undefined;
|
|
536
|
+
if (credential?.key)
|
|
537
|
+
return { type: "api_key", source: "stored credential" };
|
|
538
|
+
const configured = await resolveCredentials({ ...definition, apiKeyConfig: undefined, useDefaultEnv: false }, { executeHelpers: false });
|
|
539
|
+
if (configured.apiKey || configured.apiKeyConfig)
|
|
540
|
+
return {
|
|
541
|
+
type: "api_key",
|
|
542
|
+
source: definition.useGcloudTokenAuth && isGcloudTokenAuthEnabled()
|
|
543
|
+
? "gcloud ADC"
|
|
544
|
+
: (configured.apiKeyConfig ?? ENV_API_KEY),
|
|
545
|
+
};
|
|
546
|
+
if (definition.apiKeyConfig) {
|
|
547
|
+
const configuredKey = definition.apiKeyConfig.startsWith("!")
|
|
548
|
+
? definition.apiKeyConfig
|
|
549
|
+
: await resolveTemplateConfigValueFromContext(definition.apiKeyConfig, ctx.env);
|
|
550
|
+
if (configuredKey)
|
|
551
|
+
return { type: "api_key", source: definition.apiKeyConfig };
|
|
552
|
+
}
|
|
553
|
+
if (definition.useDefaultEnv && cleanConfig(await ctx.env(ENV_API_KEY_HELPER)))
|
|
554
|
+
return { type: "api_key", source: ENV_API_KEY_HELPER };
|
|
555
|
+
return definition.useDefaultEnv && cleanConfig(await ctx.env(ENV_API_KEY))
|
|
556
|
+
? { type: "api_key", source: ENV_API_KEY }
|
|
557
|
+
: undefined;
|
|
558
|
+
},
|
|
559
|
+
resolve: ({ ctx, credential }) => resolveApiKeyAuth(definition, ctx, credential),
|
|
560
|
+
},
|
|
561
|
+
oauth: definition.enableOAuth
|
|
562
|
+
? {
|
|
563
|
+
name: "LiteLLM SSO",
|
|
564
|
+
loginLabel: "Sign in with LiteLLM SSO",
|
|
565
|
+
login: (interaction) => loginOAuth(interaction, resolveHeaders(definition)),
|
|
566
|
+
refresh: async (credential, signal) => ({
|
|
567
|
+
...(await refreshLiteLLM(credential, signal)),
|
|
568
|
+
type: "oauth",
|
|
569
|
+
}),
|
|
570
|
+
toAuth: async (credential) => ({
|
|
571
|
+
apiKey: credential.access,
|
|
572
|
+
baseUrl: credential.baseUrl ? `${normalizeBaseUrl(String(credential.baseUrl))}/v1` : undefined,
|
|
573
|
+
headers: resolveHeaders(definition),
|
|
574
|
+
}),
|
|
575
|
+
}
|
|
576
|
+
: undefined,
|
|
577
|
+
};
|
|
630
578
|
}
|
|
631
579
|
function isReasoningItem(item) {
|
|
632
580
|
return typeof item === "object" && item !== null && item.type === "reasoning";
|
|
@@ -750,7 +698,10 @@ function normalizeThinkTags(message, litellmProviderNames) {
|
|
|
750
698
|
return { ...message, content };
|
|
751
699
|
}
|
|
752
700
|
export default async function (pi) {
|
|
753
|
-
const
|
|
701
|
+
const settings = await readGlobalLiteLLMSettings();
|
|
702
|
+
const definitions = getProviderDefinitions(settings);
|
|
703
|
+
const skillsEnabled = isFeatureEnabled(settings, "skills");
|
|
704
|
+
const mcpEnabled = isFeatureEnabled(settings, "mcp");
|
|
754
705
|
const providerNames = new Set(definitions.map((definition) => definition.name));
|
|
755
706
|
function discoveryDisabledReason() {
|
|
756
707
|
if (isOffline())
|
|
@@ -759,312 +710,173 @@ export default async function (pi) {
|
|
|
759
710
|
return `${ENV_TIMEOUT}=0`;
|
|
760
711
|
return null;
|
|
761
712
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
cache.headersFingerprint === headersFp;
|
|
775
|
-
let models = cacheValid && cache ? cache.models : [];
|
|
776
|
-
const shouldFetch = creds.baseUrl !== undefined &&
|
|
777
|
-
fp !== undefined &&
|
|
778
|
-
!isOffline() &&
|
|
779
|
-
getDiscoveryTimeoutMs() > 0 &&
|
|
780
|
-
(!cacheValid || isListModelsMode());
|
|
781
|
-
let credentialWarning;
|
|
782
|
-
let liveDiscoveryApiKey;
|
|
783
|
-
if (shouldFetch) {
|
|
784
|
-
try {
|
|
785
|
-
creds = await resolveCredentials(definition);
|
|
786
|
-
fp = creds.apiKeyFingerprint;
|
|
787
|
-
}
|
|
788
|
-
catch (error) {
|
|
789
|
-
// A broken alias must not abort activation for the other providers;
|
|
790
|
-
// only the default provider keeps the historical fail-fast behavior.
|
|
791
|
-
if ((!cacheValid || !cache) && definition.useDefaultEnv)
|
|
792
|
-
throw error;
|
|
793
|
-
credentialWarning = error instanceof Error ? error.message : String(error);
|
|
794
|
-
if (cacheValid && cache) {
|
|
795
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${credentialWarning}); using cached models.\n`);
|
|
796
|
-
models = cache.models;
|
|
797
|
-
}
|
|
798
|
-
else {
|
|
799
|
-
process.stderr.write(`LiteLLM (${definition.name}): credential resolution failed (${credentialWarning}); registering provider with no models.\n`);
|
|
800
|
-
models = [];
|
|
801
|
-
}
|
|
802
|
-
}
|
|
713
|
+
function requestBaseUrl(definition) {
|
|
714
|
+
const baseUrl = cleanConfig(definition.baseUrl) ??
|
|
715
|
+
(definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined) ??
|
|
716
|
+
"https://litellm.example.com";
|
|
717
|
+
return `${normalizeBaseUrl(baseUrl)}/v1`;
|
|
718
|
+
}
|
|
719
|
+
async function authForCredential(definition, credential) {
|
|
720
|
+
if (credential.type === "oauth") {
|
|
721
|
+
const baseUrl = typeof credential.baseUrl === "string"
|
|
722
|
+
? normalizeBaseUrl(credential.baseUrl)
|
|
723
|
+
: normalizeBaseUrl(requestBaseUrl(definition));
|
|
724
|
+
return { baseUrl, apiKey: credential.access, headers: resolveHeaders(definition) };
|
|
803
725
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
timeoutMs,
|
|
808
|
-
headers,
|
|
809
|
-
});
|
|
810
|
-
if (warning) {
|
|
811
|
-
if (cacheValid && cache) {
|
|
812
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${warning}); using cached models.\n`);
|
|
813
|
-
models = cache.models;
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${warning}); registering provider with no models.\n`);
|
|
817
|
-
models = [];
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
models = result.models;
|
|
822
|
-
liveDiscoveryApiKey = creds.apiKey;
|
|
823
|
-
const next = {
|
|
824
|
-
baseUrl: creds.baseUrl,
|
|
825
|
-
apiKeyFingerprint: fp,
|
|
826
|
-
headersFingerprint: headersFp,
|
|
827
|
-
fetchedAt: Date.now(),
|
|
828
|
-
source: result.source,
|
|
829
|
-
models: result.models,
|
|
830
|
-
};
|
|
831
|
-
await writeCache(getCachePath(definition.name), next);
|
|
832
|
-
cacheFetchedAt = next.fetchedAt;
|
|
833
|
-
if (isListModelsMode()) {
|
|
834
|
-
process.stderr.write(`LiteLLM (${definition.name}): ${result.models.length} models discovered (source: ${result.source}).\n`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
726
|
+
const resolved = await resolveApiKeyAuth(definition, { env: async (name) => process.env[name] }, credential);
|
|
727
|
+
if (!resolved?.auth.apiKey) {
|
|
728
|
+
throw new Error(`no credentials for ${definition.name}. Run /login litellm or set env vars.`);
|
|
837
729
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const defaultState = providerStates.find((state) => state.definition.name === PROVIDER_NAME) ?? providerStates[0];
|
|
844
|
-
let updateCosts = () => undefined;
|
|
845
|
-
const providerModelsForCosts = () => providerStates.map((state) => ({ provider: state.definition.name, models: state.models }));
|
|
846
|
-
const updateAllCosts = () => updateCosts(providerModelsForCosts());
|
|
847
|
-
function defaultApiKeyConfig(definition) {
|
|
848
|
-
if (definition.useDefaultEnv) {
|
|
849
|
-
return definition.apiKeyConfig ?? getApiKeyHelperCommand() ?? `$${ENV_API_KEY}`;
|
|
850
|
-
}
|
|
851
|
-
// An alias must never inherit the default provider's env key; leaving the
|
|
852
|
-
// key unset makes requests fail loudly instead of leaking credentials.
|
|
853
|
-
return definition.apiKeyConfig;
|
|
730
|
+
return {
|
|
731
|
+
baseUrl: normalizeBaseUrl(resolved.auth.baseUrl ?? requestBaseUrl(definition)),
|
|
732
|
+
apiKey: resolved.auth.apiKey,
|
|
733
|
+
headers: resolved.auth.headers,
|
|
734
|
+
};
|
|
854
735
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
headers:
|
|
863
|
-
models,
|
|
864
|
-
oauth: definition.enableOAuth ? oauth : undefined,
|
|
736
|
+
let registeredMcpIdentity;
|
|
737
|
+
let mcpRegistration;
|
|
738
|
+
let defaultRuntimeAuth;
|
|
739
|
+
function mcpCatalogIdentity(auth) {
|
|
740
|
+
return JSON.stringify({
|
|
741
|
+
baseUrl: normalizeBaseUrl(auth.baseUrl),
|
|
742
|
+
apiKey: auth.apiKey,
|
|
743
|
+
headers: Object.entries(auth.headers ?? {}).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
865
744
|
});
|
|
866
745
|
}
|
|
867
|
-
function
|
|
746
|
+
async function getRuntimeAuth(ctx) {
|
|
747
|
+
const auth = await ctx.modelRegistry.getProviderAuth(PROVIDER_NAME);
|
|
748
|
+
if (!auth)
|
|
749
|
+
return undefined;
|
|
750
|
+
const provider = ctx.modelRegistry.getProvider(PROVIDER_NAME);
|
|
751
|
+
const baseUrl = auth.auth.baseUrl ?? provider?.baseUrl;
|
|
752
|
+
const apiKey = auth.auth.apiKey;
|
|
753
|
+
if (!baseUrl || !apiKey)
|
|
754
|
+
return undefined;
|
|
755
|
+
const headers = Object.fromEntries(Object.entries(auth.auth.headers ?? provider?.headers ?? {}).filter((entry) => typeof entry[1] === "string"));
|
|
868
756
|
return {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
defaultState.cacheFetchedAt = next.fetchedAt;
|
|
873
|
-
const overridden = await applyOverrides(PROVIDER_NAME, next.models);
|
|
874
|
-
defaultState.models = overridden;
|
|
875
|
-
defaultState.creds = {
|
|
876
|
-
...defaultState.creds,
|
|
877
|
-
baseUrl: next.baseUrl,
|
|
878
|
-
apiKeyFingerprint: next.apiKeyFingerprint,
|
|
879
|
-
};
|
|
880
|
-
registerProvider(defaultState, overridden);
|
|
881
|
-
updateAllCosts();
|
|
882
|
-
},
|
|
757
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
758
|
+
apiKey,
|
|
759
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
883
760
|
};
|
|
884
761
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (!
|
|
898
|
-
|
|
899
|
-
|
|
762
|
+
async function resolveDefaultRuntimeAuth(ctx) {
|
|
763
|
+
if (ctx?.modelRegistry) {
|
|
764
|
+
const auth = await getRuntimeAuth(ctx);
|
|
765
|
+
if (auth)
|
|
766
|
+
return auth;
|
|
767
|
+
throw new Error("no credentials for litellm. Run /login litellm or set env vars.");
|
|
768
|
+
}
|
|
769
|
+
if (!defaultRuntimeAuth)
|
|
770
|
+
throw new Error("no credentials for litellm. Run /login litellm or set env vars.");
|
|
771
|
+
return defaultRuntimeAuth;
|
|
772
|
+
}
|
|
773
|
+
function waitForMcpRegistration(registration, signal) {
|
|
774
|
+
if (!signal)
|
|
775
|
+
return registration.catch(() => undefined);
|
|
776
|
+
signal.throwIfAborted();
|
|
777
|
+
return new Promise((resolve, reject) => {
|
|
778
|
+
const onAbort = () => {
|
|
779
|
+
signal.removeEventListener("abort", onAbort);
|
|
780
|
+
reject(signal.reason);
|
|
781
|
+
};
|
|
782
|
+
const onComplete = () => {
|
|
783
|
+
signal.removeEventListener("abort", onAbort);
|
|
784
|
+
resolve();
|
|
785
|
+
};
|
|
786
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
787
|
+
if (signal.aborted)
|
|
788
|
+
onAbort();
|
|
789
|
+
else
|
|
790
|
+
registration.then(onComplete, onComplete);
|
|
791
|
+
});
|
|
900
792
|
}
|
|
901
|
-
function
|
|
902
|
-
if (!
|
|
793
|
+
async function registerMcpTools(auth, signal) {
|
|
794
|
+
if (!mcpEnabled || discoveryDisabledReason())
|
|
903
795
|
return;
|
|
904
|
-
|
|
905
|
-
|
|
796
|
+
const identity = mcpCatalogIdentity(auth);
|
|
797
|
+
while (mcpRegistration) {
|
|
798
|
+
await waitForMcpRegistration(mcpRegistration, signal);
|
|
799
|
+
signal?.throwIfAborted();
|
|
800
|
+
if (registeredMcpIdentity === identity)
|
|
801
|
+
return;
|
|
906
802
|
}
|
|
907
|
-
|
|
908
|
-
function seededRuntimeApiKey(state, seed) {
|
|
909
|
-
let first = seed;
|
|
910
|
-
return async () => {
|
|
911
|
-
if (first) {
|
|
912
|
-
const value = first;
|
|
913
|
-
first = undefined;
|
|
914
|
-
return value;
|
|
915
|
-
}
|
|
916
|
-
return resolveRuntimeApiKey(state);
|
|
917
|
-
};
|
|
918
|
-
}
|
|
919
|
-
async function registerMcpTools(state) {
|
|
920
|
-
if (!state.creds.baseUrl || !state.liveDiscoveryApiKey || discoveryDisabledReason())
|
|
803
|
+
if (registeredMcpIdentity === identity)
|
|
921
804
|
return;
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
805
|
+
const registration = (async () => {
|
|
806
|
+
try {
|
|
807
|
+
signal?.throwIfAborted();
|
|
808
|
+
const tools = await createMcpToolDefinitions((ctx) => (ctx?.modelRegistry ? resolveDefaultRuntimeAuth(ctx) : Promise.resolve(auth)), isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM MCP: ${message}\n`) : undefined, signal);
|
|
809
|
+
signal?.throwIfAborted();
|
|
810
|
+
for (const tool of tools)
|
|
811
|
+
pi.registerTool(tool);
|
|
812
|
+
registeredMcpIdentity = identity;
|
|
926
813
|
}
|
|
814
|
+
catch (error) {
|
|
815
|
+
if (signal?.aborted)
|
|
816
|
+
throw signal.reason;
|
|
817
|
+
process.stderr.write(`LiteLLM (${PROVIDER_NAME}): MCP tool discovery failed (${error instanceof Error ? error.message : String(error)}).\n`);
|
|
818
|
+
}
|
|
819
|
+
})();
|
|
820
|
+
mcpRegistration = registration;
|
|
821
|
+
try {
|
|
822
|
+
await registration;
|
|
927
823
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
registerSkillTools(defaultState);
|
|
933
|
-
await registerMcpTools(defaultState);
|
|
934
|
-
async function refreshModelsAndCosts(state) {
|
|
935
|
-
const fresh = await resolveCredentials(state.definition);
|
|
936
|
-
const freshFp = fresh.apiKeyFingerprint;
|
|
937
|
-
if (!fresh.baseUrl || !fresh.apiKey || !freshFp) {
|
|
938
|
-
throw new Error(`no credentials for ${state.definition.name}. Run /login litellm or set env vars.`);
|
|
824
|
+
finally {
|
|
825
|
+
if (mcpRegistration === registration)
|
|
826
|
+
mcpRegistration = undefined;
|
|
939
827
|
}
|
|
940
|
-
const result = await discoverModels(fresh.baseUrl, fresh.apiKey, {
|
|
941
|
-
timeoutMs: getDiscoveryTimeoutMs(),
|
|
942
|
-
headers: state.headers,
|
|
943
|
-
});
|
|
944
|
-
const now = Date.now();
|
|
945
|
-
await writeCache(getCachePath(state.definition.name), {
|
|
946
|
-
baseUrl: fresh.baseUrl,
|
|
947
|
-
apiKeyFingerprint: freshFp,
|
|
948
|
-
headersFingerprint: computeHeadersFingerprint(state.headers),
|
|
949
|
-
fetchedAt: now,
|
|
950
|
-
source: result.source,
|
|
951
|
-
models: result.models,
|
|
952
|
-
});
|
|
953
|
-
const overridden = await applyOverrides(state.definition.name, result.models);
|
|
954
|
-
state.creds = fresh;
|
|
955
|
-
state.models = overridden;
|
|
956
|
-
state.liveDiscoveryApiKey = fresh.apiKey;
|
|
957
|
-
state.cacheFetchedAt = now;
|
|
958
|
-
registerProvider(state, overridden, fresh.apiKeyConfig);
|
|
959
|
-
updateAllCosts();
|
|
960
|
-
if (state.definition.name === PROVIDER_NAME)
|
|
961
|
-
await registerMcpTools(state);
|
|
962
|
-
return { providerName: state.definition.name, models: overridden, source: result.source };
|
|
963
|
-
}
|
|
964
|
-
function runRefresh(state) {
|
|
965
|
-
state.refreshInProgress ??= refreshModelsAndCosts(state).finally(() => {
|
|
966
|
-
state.refreshInProgress = null;
|
|
967
|
-
});
|
|
968
|
-
return state.refreshInProgress;
|
|
969
828
|
}
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
829
|
+
for (const definition of definitions) {
|
|
830
|
+
const provider = createLiteLLMProvider({
|
|
831
|
+
id: definition.name,
|
|
832
|
+
name: definition.displayName,
|
|
833
|
+
baseUrl: requestBaseUrl(definition),
|
|
834
|
+
auth: createProviderAuth(definition),
|
|
835
|
+
discover: async (credential, signal) => {
|
|
836
|
+
const disabledReason = discoveryDisabledReason();
|
|
837
|
+
if (disabledReason)
|
|
838
|
+
throw new Error(`discovery disabled (${disabledReason})`);
|
|
839
|
+
const auth = await authForCredential(definition, credential);
|
|
840
|
+
const result = await discoverModels(auth.baseUrl, auth.apiKey, {
|
|
841
|
+
...getModelsDevDiscoveryOptions(),
|
|
842
|
+
timeoutMs: getDiscoveryTimeoutMs(),
|
|
843
|
+
signal,
|
|
844
|
+
headers: auth.headers,
|
|
845
|
+
silent: !isVerboseDiscovery(),
|
|
846
|
+
onProgress: isVerboseDiscovery() ? (message) => process.stderr.write(`LiteLLM: ${message}\n`) : undefined,
|
|
847
|
+
});
|
|
848
|
+
signal?.throwIfAborted();
|
|
849
|
+
return { ...result, baseUrl: `${normalizeBaseUrl(auth.baseUrl)}/v1` };
|
|
982
850
|
},
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const credentialBaseUrl = credential.baseUrl;
|
|
990
|
-
const credentialAccess = typeof credential.access === "string" ? credential.access : undefined;
|
|
991
|
-
defaultState.creds = { ...defaultState.creds, baseUrl: credentialBaseUrl };
|
|
992
|
-
defaultState.liveDiscoveryApiKey = credentialAccess;
|
|
993
|
-
registerSkillTools(defaultState);
|
|
994
|
-
await registerMcpTools(defaultState);
|
|
995
|
-
ctx.ui.notify(`Logged in to LiteLLM. Credentials saved to ${getAuthPath()}`, "info");
|
|
996
|
-
}
|
|
997
|
-
pi.on("input", async (event, ctx) => {
|
|
998
|
-
if (event.text.trim() !== `/login ${PROVIDER_NAME}`)
|
|
999
|
-
return { action: "continue" };
|
|
1000
|
-
if (!ctx.hasUI)
|
|
1001
|
-
return { action: "continue" };
|
|
1002
|
-
try {
|
|
1003
|
-
await runLogin(ctx);
|
|
1004
|
-
}
|
|
1005
|
-
catch (error) {
|
|
1006
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1007
|
-
if (message !== "Login cancelled")
|
|
1008
|
-
ctx.ui.notify(`LiteLLM login failed: ${message}`, "error");
|
|
1009
|
-
}
|
|
1010
|
-
return { action: "handled" };
|
|
1011
|
-
});
|
|
1012
|
-
pi.registerCommand("litellm-refresh", {
|
|
1013
|
-
description: "Re-discover models from the LiteLLM proxy.",
|
|
1014
|
-
handler: async (args, ctx) => {
|
|
1015
|
-
const disabledReason = discoveryDisabledReason();
|
|
1016
|
-
if (disabledReason) {
|
|
1017
|
-
ctx.ui.notify(`LiteLLM refresh disabled (${disabledReason})`, "warning");
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
const requestedProvider = args.trim();
|
|
1021
|
-
const statesToRefresh = requestedProvider
|
|
1022
|
-
? providerStates.filter((state) => state.definition.name === requestedProvider)
|
|
1023
|
-
: providerStates;
|
|
1024
|
-
if (statesToRefresh.length === 0) {
|
|
1025
|
-
ctx.ui.notify(`LiteLLM refresh failed: unknown provider ${requestedProvider}`, "error");
|
|
1026
|
-
return;
|
|
851
|
+
});
|
|
852
|
+
Object.assign(provider, { headers: resolveHeaders(definition) });
|
|
853
|
+
const refreshModels = provider.refreshModels;
|
|
854
|
+
provider.refreshModels = async (context) => {
|
|
855
|
+
try {
|
|
856
|
+
await refreshModels(context);
|
|
1027
857
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
});
|
|
1037
|
-
if (failed.length === 0) {
|
|
1038
|
-
if (succeeded.length === 1) {
|
|
1039
|
-
const result = succeeded[0];
|
|
1040
|
-
ctx.ui.notify(`LiteLLM: ${result.models.length} models refreshed (source: ${result.source})`, "info");
|
|
1041
|
-
return;
|
|
858
|
+
finally {
|
|
859
|
+
if (definition.name === PROVIDER_NAME &&
|
|
860
|
+
context.allowNetwork &&
|
|
861
|
+
!discoveryDisabledReason() &&
|
|
862
|
+
context.credential) {
|
|
863
|
+
const auth = await authForCredential(definition, context.credential);
|
|
864
|
+
defaultRuntimeAuth = auth;
|
|
865
|
+
await registerMcpTools(auth, context.signal);
|
|
1042
866
|
}
|
|
1043
|
-
ctx.ui.notify(`LiteLLM: ${succeeded.length} providers refreshed (${succeeded
|
|
1044
|
-
.map((result) => `${result.providerName}: ${result.models.length} models`)
|
|
1045
|
-
.join(", ")})`, "info");
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
const failures = failed.map(({ name, message }) => (settled.length === 1 ? message : `${name}: ${message}`));
|
|
1049
|
-
if (succeeded.length === 0) {
|
|
1050
|
-
ctx.ui.notify(`LiteLLM refresh failed: ${failures.join("; ")}`, "error");
|
|
1051
|
-
return;
|
|
1052
867
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
868
|
+
};
|
|
869
|
+
pi.registerProvider(provider);
|
|
870
|
+
}
|
|
871
|
+
setupLiteLLMCostTracking(pi, [...providerNames]);
|
|
872
|
+
if (skillsEnabled) {
|
|
873
|
+
for (const tool of createSkillToolDefinitions(resolveDefaultRuntimeAuth)) {
|
|
874
|
+
pi.registerTool(tool);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
1058
877
|
let sessionId;
|
|
1059
878
|
pi.on("session_start", (_event, ctx) => {
|
|
1060
879
|
sessionId = getSessionIdFromFile(ctx.sessionManager.getSessionFile());
|
|
1061
|
-
if (discoveryDisabledReason())
|
|
1062
|
-
return;
|
|
1063
|
-
for (const state of providerStates) {
|
|
1064
|
-
if (!state.cacheFetchedAt || Date.now() - state.cacheFetchedAt <= CACHE_STALE_MS)
|
|
1065
|
-
continue;
|
|
1066
|
-
void runRefresh(state).catch(() => undefined);
|
|
1067
|
-
}
|
|
1068
880
|
});
|
|
1069
881
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1070
882
|
if (!ctx.model?.provider || !providerNames.has(ctx.model.provider))
|
|
@@ -1073,13 +885,15 @@ export default async function (pi) {
|
|
|
1073
885
|
return;
|
|
1074
886
|
return prepareLiteLLMRequestPayload(event.payload, ctx.model?.id, ctx.model?.api, sessionId);
|
|
1075
887
|
});
|
|
1076
|
-
pi.on("before_agent_start", async (event) => {
|
|
1077
|
-
|
|
888
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
889
|
+
defaultRuntimeAuth = undefined;
|
|
890
|
+
if (!skillsEnabled || discoveryDisabledReason())
|
|
1078
891
|
return;
|
|
1079
|
-
const
|
|
1080
|
-
if (!
|
|
892
|
+
const auth = await getRuntimeAuth(ctx);
|
|
893
|
+
if (!auth)
|
|
1081
894
|
return;
|
|
1082
|
-
|
|
895
|
+
defaultRuntimeAuth = auth;
|
|
896
|
+
const skills = await listSkills(defaultRuntimeAuth.baseUrl, defaultRuntimeAuth.apiKey, defaultRuntimeAuth.headers);
|
|
1083
897
|
const section = createSkillsPromptSection(skills);
|
|
1084
898
|
if (!section)
|
|
1085
899
|
return;
|