pi-provider-litellm 1.3.0 → 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 +18 -20
- package/dist/cache.d.ts +1 -5
- package/dist/cache.js +3 -42
- package/dist/cost.d.ts +2 -6
- package/dist/cost.js +0 -0
- package/dist/discover.d.ts +3 -7
- package/dist/discover.js +183 -77
- package/dist/index.js +443 -616
- package/dist/litellm.js +2 -4
- package/dist/mcp-tools.d.ts +4 -4
- package/dist/mcp-tools.js +14 -28
- 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 +12 -9
- package/package.json +7 -7
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 { getAgentDir
|
|
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,175 +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
|
-
const apiKey = credential.type === "oauth" ? credential.access : credential.key;
|
|
29
|
-
const credentialBaseUrl = credential.type === "oauth" ? credential.baseUrl : undefined;
|
|
30
|
-
const fingerprintSource = credential.type === "api_key" && state.creds.apiKeyConfig?.startsWith("!")
|
|
31
|
-
? state.creds.apiKeyConfig
|
|
32
|
-
: credential.type === "oauth" && credential.refresh.startsWith("!")
|
|
33
|
-
? credential.refresh
|
|
34
|
-
: apiKey;
|
|
28
|
+
function getModelsDevDiscoveryOptions() {
|
|
35
29
|
return {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
apiKey,
|
|
39
|
-
apiKeyFingerprint: fingerprintSource ? fingerprint(fingerprintSource) : undefined,
|
|
30
|
+
modelsDev: process.env[ENV_MODELS_DEV] !== "0",
|
|
31
|
+
modelsDevCachePath: join(getAgentDir(), MODELS_DEV_CACHE_FILENAME),
|
|
40
32
|
};
|
|
41
33
|
}
|
|
42
|
-
function getAuthPath() {
|
|
43
|
-
return join(getAgentDir(), "auth.json");
|
|
44
|
-
}
|
|
45
|
-
function sanitizeCacheSegment(value) {
|
|
46
|
-
return (value
|
|
47
|
-
.toLowerCase()
|
|
48
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
49
|
-
.replace(/^-+|-+$/g, "") || "provider");
|
|
50
|
-
}
|
|
51
|
-
function getCachePath(providerName = PROVIDER_NAME) {
|
|
52
|
-
if (providerName === PROVIDER_NAME)
|
|
53
|
-
return join(getAgentDir(), CACHE_FILENAME);
|
|
54
|
-
return join(getAgentDir(), `litellm-models-${sanitizeCacheSegment(providerName)}.json`);
|
|
55
|
-
}
|
|
56
|
-
// Same tolerance as pi core's models.json loader (stripJsonComments in dist/utils/json.js):
|
|
57
|
-
// strip `//` line comments and trailing commas, leaving string literals untouched.
|
|
58
|
-
function stripJsonComments(input) {
|
|
59
|
-
return input
|
|
60
|
-
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
|
61
|
-
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? m);
|
|
62
|
-
}
|
|
63
34
|
function isPlainObject(value) {
|
|
64
35
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
36
|
}
|
|
66
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
67
|
-
const COST_RATE_FIELDS = ["input", "output", "cacheRead", "cacheWrite"];
|
|
68
|
-
const COST_TIER_FIELDS = [...COST_RATE_FIELDS, "inputTokensAbove"];
|
|
69
|
-
// Mirrors pi core's ModelOverrideSchema: core rejects the whole models.json on invalid values,
|
|
70
|
-
// so anything dropped here would also have been flagged for built-in providers. Headers matter
|
|
71
|
-
// most: pi resolves each header value as a config string, so non-strings break requests.
|
|
72
|
-
const MODEL_OVERRIDE_VALIDATORS = {
|
|
73
|
-
name: (value) => typeof value === "string",
|
|
74
|
-
reasoning: (value) => typeof value === "boolean",
|
|
75
|
-
thinkingLevelMap: (value) => isPlainObject(value) && THINKING_LEVELS.every((level) => value[level] == null || typeof value[level] === "string"),
|
|
76
|
-
input: (value) => Array.isArray(value) && value.every((entry) => entry === "text" || entry === "image"),
|
|
77
|
-
contextWindow: (value) => typeof value === "number",
|
|
78
|
-
maxTokens: (value) => typeof value === "number",
|
|
79
|
-
headers: (value) => isPlainObject(value) && Object.values(value).every((entry) => typeof entry === "string"),
|
|
80
|
-
compat: isPlainObject,
|
|
81
|
-
cost: (value) => isPlainObject(value) &&
|
|
82
|
-
Object.entries(value).every(([key, entry]) => key === "tiers"
|
|
83
|
-
? Array.isArray(entry) &&
|
|
84
|
-
entry.every((tier) => isPlainObject(tier) &&
|
|
85
|
-
COST_TIER_FIELDS.every((field) => typeof tier[field] === "number") &&
|
|
86
|
-
Object.keys(tier).every((field) => COST_TIER_FIELDS.includes(field)))
|
|
87
|
-
: COST_RATE_FIELDS.includes(key) && typeof entry === "number"),
|
|
88
|
-
};
|
|
89
|
-
function sanitizeModelOverride(modelId, raw) {
|
|
90
|
-
if (!isPlainObject(raw)) {
|
|
91
|
-
process.stderr.write(`LiteLLM: ignoring model override for ${modelId} in models.json (not an object).\n`);
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
const override = {};
|
|
95
|
-
const dropped = [];
|
|
96
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
97
|
-
const isValid = MODEL_OVERRIDE_VALIDATORS[key];
|
|
98
|
-
if (isValid?.(value))
|
|
99
|
-
override[key] = value;
|
|
100
|
-
else
|
|
101
|
-
dropped.push(key);
|
|
102
|
-
}
|
|
103
|
-
if (dropped.length > 0) {
|
|
104
|
-
process.stderr.write(`LiteLLM: ignoring invalid model override field(s) for ${modelId} in models.json: ${dropped.join(", ")}.\n`);
|
|
105
|
-
}
|
|
106
|
-
return override;
|
|
107
|
-
}
|
|
108
|
-
// Overrides are read per registered provider name, matching pi core's own
|
|
109
|
-
// models.json layout (`providers.<name>.modelOverrides`) so aliases get the
|
|
110
|
-
// same override support as the default provider, not a special case of it.
|
|
111
|
-
async function readModelOverrides(providerName) {
|
|
112
|
-
let raw;
|
|
113
|
-
try {
|
|
114
|
-
raw = await readFile(join(getAgentDir(), "models.json"), "utf8");
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
return new Map();
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
const config = JSON.parse(stripJsonComments(raw));
|
|
121
|
-
const overrides = new Map();
|
|
122
|
-
for (const [id, rawOverride] of Object.entries(config.providers?.[providerName]?.modelOverrides ?? {})) {
|
|
123
|
-
const override = sanitizeModelOverride(id, rawOverride);
|
|
124
|
-
if (override)
|
|
125
|
-
overrides.set(id, override);
|
|
126
|
-
}
|
|
127
|
-
return overrides;
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
-
process.stderr.write(`LiteLLM: ignoring model overrides (failed to parse models.json: ${message}).\n`);
|
|
132
|
-
return new Map();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Merge semantics must match pi core's applyModelOverride/mergeCompat (dist/core/model-registry.js)
|
|
136
|
-
// so the same models.json entry behaves identically for litellm and built-in providers.
|
|
137
|
-
function mergeCompat(base, override) {
|
|
138
|
-
if (!override)
|
|
139
|
-
return base;
|
|
140
|
-
const merged = { ...base, ...override };
|
|
141
|
-
for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs"]) {
|
|
142
|
-
const baseValue = base?.[key];
|
|
143
|
-
const overrideValue = override[key];
|
|
144
|
-
if (baseValue || overrideValue) {
|
|
145
|
-
merged[key] = { ...baseValue, ...overrideValue };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return merged;
|
|
149
|
-
}
|
|
150
|
-
function applyModelOverride(model, override) {
|
|
151
|
-
const next = { ...model };
|
|
152
|
-
if (override.name !== undefined)
|
|
153
|
-
next.name = override.name;
|
|
154
|
-
if (override.reasoning !== undefined)
|
|
155
|
-
next.reasoning = override.reasoning;
|
|
156
|
-
if (override.thinkingLevelMap !== undefined) {
|
|
157
|
-
next.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap };
|
|
158
|
-
}
|
|
159
|
-
if (override.input !== undefined)
|
|
160
|
-
next.input = override.input;
|
|
161
|
-
if (override.contextWindow !== undefined)
|
|
162
|
-
next.contextWindow = override.contextWindow;
|
|
163
|
-
if (override.maxTokens !== undefined)
|
|
164
|
-
next.maxTokens = override.maxTokens;
|
|
165
|
-
if (override.headers !== undefined)
|
|
166
|
-
next.headers = override.headers;
|
|
167
|
-
if (override.compat !== undefined)
|
|
168
|
-
next.compat = mergeCompat(model.compat, override.compat);
|
|
169
|
-
if (override.cost !== undefined)
|
|
170
|
-
next.cost = { ...model.cost, ...override.cost };
|
|
171
|
-
return next;
|
|
172
|
-
}
|
|
173
|
-
function applyModelOverrides(models, overrides) {
|
|
174
|
-
if (overrides.size === 0)
|
|
175
|
-
return models;
|
|
176
|
-
return models.map((model) => {
|
|
177
|
-
const override = overrides.get(model.id);
|
|
178
|
-
return override ? applyModelOverride(model, override) : model;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
// Re-reads models.json on every call so overrides edited mid-session take effect on the next
|
|
182
|
-
// refresh or login, matching pi core's live reload for built-in providers.
|
|
183
|
-
async function applyOverrides(providerName, models) {
|
|
184
|
-
return applyModelOverrides(models, await readModelOverrides(providerName));
|
|
185
|
-
}
|
|
186
|
-
function readAuthEntry(providerName = PROVIDER_NAME) {
|
|
187
|
-
return readStoredCredential(providerName, getAuthPath());
|
|
188
|
-
}
|
|
189
37
|
async function readGlobalLiteLLMSettings() {
|
|
190
38
|
try {
|
|
191
39
|
const raw = await readFile(join(getAgentDir(), "settings.json"), "utf8");
|
|
@@ -215,9 +63,50 @@ function normalizeCommand(raw) {
|
|
|
215
63
|
function getApiKeyHelperCommand() {
|
|
216
64
|
return normalizeCommand(process.env[ENV_API_KEY_HELPER]);
|
|
217
65
|
}
|
|
218
|
-
function
|
|
66
|
+
function parseApiKeyCommand(commandConfig) {
|
|
219
67
|
const command = commandConfig.startsWith("!") ? commandConfig.slice(1) : commandConfig;
|
|
220
|
-
|
|
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();
|
|
221
110
|
if (!output)
|
|
222
111
|
throw new Error(`LiteLLM API key helper produced no output: ${command}`);
|
|
223
112
|
return output;
|
|
@@ -237,39 +126,28 @@ function tokenExpiresAt(apiKey, opaqueFallback = PERMANENT_TOKEN_EXPIRES_AT) {
|
|
|
237
126
|
}
|
|
238
127
|
}
|
|
239
128
|
async function generateVirtualKey(baseUrl, userToken, signal, headers) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
const data = (await response.json());
|
|
257
|
-
if (typeof data.key !== "string" || !data.key)
|
|
258
|
-
throw new Error("No key in response from /key/generate");
|
|
259
|
-
const expiresMs = typeof data.expires === "string" ? Date.parse(data.expires) : Number.NaN;
|
|
260
|
-
return { key: data.key, expiresAt: Number.isNaN(expiresMs) ? undefined : expiresMs };
|
|
261
|
-
}
|
|
262
|
-
finally {
|
|
263
|
-
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}`);
|
|
264
145
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return credentials.refresh.startsWith("!")
|
|
271
|
-
? executeApiKeyCommand(credentials.refresh)
|
|
272
|
-
: 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 };
|
|
273
151
|
}
|
|
274
152
|
function resolveTemplateConfigValue(config) {
|
|
275
153
|
let resolved = "";
|
|
@@ -313,6 +191,47 @@ function resolveTemplateConfigValue(config) {
|
|
|
313
191
|
}
|
|
314
192
|
return resolved;
|
|
315
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
|
+
}
|
|
316
235
|
function resolveConfigValue(config, { executeCommands }) {
|
|
317
236
|
if (config.startsWith("!"))
|
|
318
237
|
return executeCommands ? executeApiKeyCommand(config) : undefined;
|
|
@@ -364,97 +283,56 @@ function resolveHeaders(definition) {
|
|
|
364
283
|
return parseCustomHeaders(resolveTemplateConfigValue(definition.headers));
|
|
365
284
|
return parseHeaderRecord(definition.headers);
|
|
366
285
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (!headers)
|
|
372
|
-
return undefined;
|
|
373
|
-
const sorted = Object.keys(headers)
|
|
374
|
-
.sort()
|
|
375
|
-
.map((key) => [key, headers[key]]);
|
|
376
|
-
return fingerprint(JSON.stringify(sorted));
|
|
377
|
-
}
|
|
378
|
-
// Pi core resolves registered header values with the same $VAR/!command syntax
|
|
379
|
-
// at request time, so already-resolved literals must be escaped or they get
|
|
380
|
-
// resolved a second time ($UNSET would then fail every request).
|
|
381
|
-
function escapeConfigValue(value) {
|
|
382
|
-
const escaped = value.replace(/\$/g, "$$$$");
|
|
383
|
-
return escaped.startsWith("!") ? `$${escaped}` : escaped;
|
|
384
|
-
}
|
|
385
|
-
function escapeHeaderConfig(headers) {
|
|
386
|
-
if (!headers)
|
|
387
|
-
return undefined;
|
|
388
|
-
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);
|
|
389
290
|
}
|
|
390
291
|
async function resolveCredentials(definition, { executeHelpers = true } = {}) {
|
|
391
|
-
const entry = definition.useSavedAuth ? await readAuthEntry(definition.name) : undefined;
|
|
392
292
|
const configuredBase = cleanConfig(definition.baseUrl) ?? (definition.useDefaultEnv ? cleanConfig(process.env[ENV_BASE_URL]) : undefined);
|
|
393
293
|
const envKey = definition.useDefaultEnv ? cleanConfig(process.env[ENV_API_KEY]) : undefined;
|
|
394
294
|
const envHelperCommand = definition.useDefaultEnv ? getApiKeyHelperCommand() : undefined;
|
|
395
295
|
const useGcloudToken = definition.useGcloudTokenAuth && isGcloudTokenAuthEnabled();
|
|
396
|
-
const
|
|
397
|
-
const gcloudCacheKey = useGcloudToken && !entry ? ((await getGcloudTokenCacheKey()) ?? undefined) : undefined;
|
|
398
|
-
const authKey = entry?.type === "oauth"
|
|
399
|
-
? (executeHelpers ? resolveOAuthApiKey(entry) : entry.access).trim()
|
|
400
|
-
: entry?.type === "api_key" && typeof entry.key === "string"
|
|
401
|
-
? resolveConfigValue(entry.key, { executeCommands: executeHelpers })?.trim()
|
|
402
|
-
: undefined;
|
|
296
|
+
const gcloudCacheKey = useGcloudToken ? ((await getGcloudTokenCacheKey()) ?? undefined) : undefined;
|
|
403
297
|
const gcloudKey = executeHelpers && gcloudCacheKey ? (await getGcloudToken())?.trim() : undefined;
|
|
404
298
|
// Resolved lazily so a `!command` key is not executed when a
|
|
405
299
|
// higher-precedence credential (saved auth, gcloud token) already won.
|
|
406
300
|
let configuredKey;
|
|
407
|
-
if (!
|
|
301
|
+
if (!gcloudKey && definition.apiKeyConfig) {
|
|
408
302
|
configuredKey = resolveConfigValue(definition.apiKeyConfig, { executeCommands: executeHelpers });
|
|
409
303
|
if (configuredKey === undefined && !definition.apiKeyConfig.startsWith("!")) {
|
|
410
304
|
warnUnresolvedApiKeyConfig(definition.name, definition.apiKeyConfig);
|
|
411
305
|
}
|
|
412
306
|
}
|
|
413
|
-
const helperKey = !
|
|
307
|
+
const helperKey = !gcloudKey && !configuredKey && executeHelpers && envHelperCommand
|
|
414
308
|
? executeApiKeyCommand(envHelperCommand)
|
|
415
309
|
: undefined;
|
|
416
|
-
const apiKey =
|
|
417
|
-
let apiKeyFingerprint;
|
|
310
|
+
const apiKey = gcloudKey || configuredKey || helperKey || envKey;
|
|
418
311
|
let apiKeyConfig;
|
|
419
|
-
if (
|
|
420
|
-
apiKeyFingerprint = fingerprint(entry.refresh);
|
|
421
|
-
}
|
|
422
|
-
else if (authKey) {
|
|
423
|
-
apiKeyFingerprint = fingerprint(authKey);
|
|
424
|
-
}
|
|
425
|
-
else if (gcloudKey) {
|
|
426
|
-
apiKeyFingerprint = fingerprint(gcloudCacheKey ?? gcloudKey);
|
|
312
|
+
if (gcloudKey) {
|
|
427
313
|
apiKeyConfig = getGcloudTokenCommand();
|
|
428
314
|
}
|
|
429
315
|
else if (!executeHelpers && gcloudCacheKey) {
|
|
430
|
-
apiKeyFingerprint = fingerprint(gcloudCacheKey);
|
|
431
316
|
apiKeyConfig = getGcloudTokenCommand();
|
|
432
317
|
}
|
|
433
318
|
else if (configuredKey && definition.apiKeyConfig) {
|
|
434
|
-
apiKeyFingerprint = fingerprint(definition.apiKeyConfig.startsWith("!") ? definition.apiKeyConfig : configuredKey);
|
|
435
319
|
apiKeyConfig = definition.apiKeyConfig;
|
|
436
320
|
}
|
|
437
321
|
else if (!executeHelpers && definition.apiKeyConfig?.startsWith("!")) {
|
|
438
|
-
apiKeyFingerprint = fingerprint(definition.apiKeyConfig);
|
|
439
322
|
apiKeyConfig = definition.apiKeyConfig;
|
|
440
323
|
}
|
|
441
324
|
else if (helperKey && envHelperCommand) {
|
|
442
|
-
apiKeyFingerprint = fingerprint(envHelperCommand);
|
|
443
325
|
apiKeyConfig = envHelperCommand;
|
|
444
326
|
}
|
|
445
327
|
else if (!executeHelpers && envHelperCommand) {
|
|
446
|
-
apiKeyFingerprint = fingerprint(envHelperCommand);
|
|
447
328
|
apiKeyConfig = envHelperCommand;
|
|
448
329
|
}
|
|
449
330
|
else if (envKey) {
|
|
450
|
-
apiKeyFingerprint = fingerprint(envKey);
|
|
451
331
|
apiKeyConfig = `$${ENV_API_KEY}`;
|
|
452
332
|
}
|
|
453
|
-
const rawBase = authBase || configuredBase;
|
|
454
333
|
return {
|
|
455
|
-
baseUrl:
|
|
334
|
+
baseUrl: configuredBase ? normalizeBaseUrl(configuredBase) : undefined,
|
|
456
335
|
apiKey: apiKey || undefined,
|
|
457
|
-
apiKeyFingerprint,
|
|
458
336
|
apiKeyConfig,
|
|
459
337
|
};
|
|
460
338
|
}
|
|
@@ -470,8 +348,8 @@ function getDiscoveryTimeoutMs() {
|
|
|
470
348
|
function isOffline() {
|
|
471
349
|
return process.env[ENV_OFFLINE] === "1";
|
|
472
350
|
}
|
|
473
|
-
function
|
|
474
|
-
return process.
|
|
351
|
+
function isVerboseDiscovery() {
|
|
352
|
+
return process.env[ENV_VERBOSE_DISCOVERY] === "1";
|
|
475
353
|
}
|
|
476
354
|
function normalizeProviderSettings(raw) {
|
|
477
355
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -497,129 +375,81 @@ function getProviderDefinitions(settings) {
|
|
|
497
375
|
headers: raw?.headers ?? (isDefault ? `$${ENV_HEADERS}` : undefined),
|
|
498
376
|
useDefaultEnv: isDefault,
|
|
499
377
|
useGcloudTokenAuth: isDefault,
|
|
500
|
-
useSavedAuth: true,
|
|
501
378
|
enableOAuth: isDefault,
|
|
502
379
|
});
|
|
503
380
|
const definitions = [makeDefinition(PROVIDER_NAME, defaultSettings, true)];
|
|
504
|
-
const usedCacheSegments = new Map();
|
|
505
381
|
for (const [name, raw] of Object.entries(providerSettings ?? {})) {
|
|
506
382
|
if (name === PROVIDER_NAME)
|
|
507
383
|
continue;
|
|
508
384
|
const normalized = normalizeProviderSettings(raw);
|
|
509
385
|
if (!normalized)
|
|
510
386
|
continue;
|
|
511
|
-
// Distinct alias names can sanitize to the same cache file; registering
|
|
512
|
-
// both would let their model caches silently clobber each other.
|
|
513
|
-
const segment = sanitizeCacheSegment(name);
|
|
514
|
-
const existing = usedCacheSegments.get(segment);
|
|
515
|
-
if (existing) {
|
|
516
|
-
process.stderr.write(`LiteLLM: provider alias "${name}" would share a cache file with "${existing}"; skipping it. Rename the alias.\n`);
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
usedCacheSegments.set(segment, name);
|
|
520
387
|
definitions.push(makeDefinition(name, normalized, false));
|
|
521
388
|
}
|
|
522
389
|
return definitions;
|
|
523
390
|
}
|
|
524
|
-
async function
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
530
|
-
return {
|
|
531
|
-
result: { models: [], source: "models_list" },
|
|
532
|
-
warning: message,
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
async function loginLiteLLM(callbacks, options) {
|
|
537
|
-
const rawBaseUrl = (await callbacks.onPrompt({
|
|
391
|
+
async function loginApiKey(interaction) {
|
|
392
|
+
const rawBaseUrl = (await interaction.prompt({
|
|
393
|
+
type: "text",
|
|
538
394
|
message: "Enter LiteLLM proxy URL (no trailing /v1):",
|
|
539
395
|
placeholder: "https://litellm.example.com",
|
|
540
396
|
})).trim();
|
|
541
397
|
if (!rawBaseUrl)
|
|
542
398
|
throw new Error("Base URL is required");
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
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",
|
|
546
409
|
})).trim();
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
catch (error) {
|
|
577
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
578
|
-
callbacks.onProgress?.(`LiteLLM: virtual key generation failed (${message}); using SSO token directly.`);
|
|
579
|
-
apiKey = rawToken;
|
|
580
|
-
refresh = "";
|
|
581
|
-
expires = tokenExpiresAt(rawToken, PERMANENT_TOKEN_EXPIRES_AT);
|
|
582
|
-
}
|
|
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." });
|
|
583
439
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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
|
+
});
|
|
588
448
|
}
|
|
589
449
|
}
|
|
590
|
-
|
|
591
|
-
const apiKeyInput = (await callbacks.onPrompt({ message: "Enter API key:" })).trim();
|
|
592
|
-
if (!apiKeyInput)
|
|
593
|
-
throw new Error("Both base URL and API key are required");
|
|
594
|
-
refresh = apiKeyInput.startsWith("!") ? apiKeyInput : "";
|
|
595
|
-
apiKey = refresh ? executeApiKeyCommand(refresh) : apiKeyInput;
|
|
596
|
-
expires = tokenExpiresAt(apiKey, refresh ? EXPIRE_TOKEN_IMMEDIATELY : PERMANENT_TOKEN_EXPIRES_AT);
|
|
597
|
-
}
|
|
598
|
-
const { models, source } = await discoverModels(baseUrl, apiKey, {
|
|
599
|
-
timeoutMs: LOGIN_TIMEOUT_MS,
|
|
600
|
-
signal: callbacks.signal,
|
|
601
|
-
headers: options.headers,
|
|
602
|
-
onProgress: (message) => callbacks.onProgress?.(`LiteLLM: ${message}`),
|
|
603
|
-
});
|
|
604
|
-
const cache = {
|
|
605
|
-
baseUrl,
|
|
606
|
-
apiKeyFingerprint: fingerprint(refresh || apiKey),
|
|
607
|
-
headersFingerprint: computeHeadersFingerprint(options.headers),
|
|
608
|
-
fetchedAt: Date.now(),
|
|
609
|
-
source,
|
|
610
|
-
models,
|
|
611
|
-
};
|
|
612
|
-
await writeCache(options.cachePath, cache);
|
|
613
|
-
await options.onCacheWrite?.(cache);
|
|
614
|
-
callbacks.onProgress?.(`LiteLLM: ${models.length} models discovered (source: ${source})`);
|
|
615
|
-
return {
|
|
616
|
-
access: apiKey,
|
|
617
|
-
refresh,
|
|
618
|
-
expires,
|
|
619
|
-
baseUrl,
|
|
620
|
-
};
|
|
450
|
+
return { type: "oauth", access, refresh: "", expires, baseUrl };
|
|
621
451
|
}
|
|
622
|
-
async function refreshLiteLLM(credentials) {
|
|
452
|
+
async function refreshLiteLLM(credentials, _signal) {
|
|
623
453
|
if (!credentials.refresh.startsWith("!")) {
|
|
624
454
|
if (credentials.expires < PERMANENT_TOKEN_EXPIRES_AT) {
|
|
625
455
|
throw new Error("LiteLLM credential cannot be refreshed; run /login litellm again");
|
|
@@ -629,11 +459,122 @@ async function refreshLiteLLM(credentials) {
|
|
|
629
459
|
const access = executeApiKeyCommand(credentials.refresh);
|
|
630
460
|
return { ...credentials, access, expires: tokenExpiresAt(access, EXPIRE_TOKEN_IMMEDIATELY) };
|
|
631
461
|
}
|
|
632
|
-
function
|
|
633
|
-
const baseUrl =
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
+
};
|
|
637
578
|
}
|
|
638
579
|
function isReasoningItem(item) {
|
|
639
580
|
return typeof item === "object" && item !== null && item.type === "reasoning";
|
|
@@ -769,289 +710,173 @@ export default async function (pi) {
|
|
|
769
710
|
return `${ENV_TIMEOUT}=0`;
|
|
770
711
|
return null;
|
|
771
712
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
cache.headersFingerprint === headersFp;
|
|
785
|
-
let models = cache?.models ?? [];
|
|
786
|
-
const canDiscover = creds.baseUrl !== undefined && fp !== undefined && !isOffline() && getDiscoveryTimeoutMs() > 0;
|
|
787
|
-
const shouldFetch = canDiscover && isListModelsMode();
|
|
788
|
-
const refreshOnStart = canDiscover && !cacheValid && !shouldFetch;
|
|
789
|
-
let credentialWarning;
|
|
790
|
-
let liveDiscoveryApiKey;
|
|
791
|
-
if (shouldFetch) {
|
|
792
|
-
try {
|
|
793
|
-
creds = await resolveCredentials(definition);
|
|
794
|
-
fp = creds.apiKeyFingerprint;
|
|
795
|
-
}
|
|
796
|
-
catch (error) {
|
|
797
|
-
// A broken alias must not abort activation for the other providers;
|
|
798
|
-
// only the default provider keeps the historical fail-fast behavior.
|
|
799
|
-
if ((!cacheValid || !cache) && definition.useDefaultEnv)
|
|
800
|
-
throw error;
|
|
801
|
-
credentialWarning = error instanceof Error ? error.message : String(error);
|
|
802
|
-
if (cacheValid && cache) {
|
|
803
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${credentialWarning}); using cached models.\n`);
|
|
804
|
-
models = cache.models;
|
|
805
|
-
}
|
|
806
|
-
else {
|
|
807
|
-
process.stderr.write(`LiteLLM (${definition.name}): credential resolution failed (${credentialWarning}); registering provider with no models.\n`);
|
|
808
|
-
models = [];
|
|
809
|
-
}
|
|
810
|
-
}
|
|
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) };
|
|
811
725
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
timeoutMs,
|
|
816
|
-
headers,
|
|
817
|
-
onProgress: (message) => process.stderr.write(`LiteLLM: ${message}\n`),
|
|
818
|
-
});
|
|
819
|
-
if (warning) {
|
|
820
|
-
if (cacheValid && cache) {
|
|
821
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${warning}); using cached models.\n`);
|
|
822
|
-
models = cache.models;
|
|
823
|
-
}
|
|
824
|
-
else {
|
|
825
|
-
process.stderr.write(`LiteLLM (${definition.name}): discovery failed (${warning}); registering provider with no models.\n`);
|
|
826
|
-
models = [];
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
models = result.models;
|
|
831
|
-
liveDiscoveryApiKey = creds.apiKey;
|
|
832
|
-
const next = {
|
|
833
|
-
baseUrl: creds.baseUrl,
|
|
834
|
-
apiKeyFingerprint: fp,
|
|
835
|
-
headersFingerprint: headersFp,
|
|
836
|
-
fetchedAt: Date.now(),
|
|
837
|
-
source: result.source,
|
|
838
|
-
models: result.models,
|
|
839
|
-
};
|
|
840
|
-
await writeCache(getCachePath(definition.name), next);
|
|
841
|
-
cacheFetchedAt = next.fetchedAt;
|
|
842
|
-
if (isListModelsMode()) {
|
|
843
|
-
process.stderr.write(`LiteLLM (${definition.name}): ${result.models.length} models discovered (source: ${result.source}).\n`);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
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.`);
|
|
846
729
|
}
|
|
847
|
-
// The cache keeps raw discovery output; overrides are applied freshly at each registration.
|
|
848
|
-
models = await applyOverrides(definition.name, models);
|
|
849
730
|
return {
|
|
850
|
-
definition,
|
|
851
|
-
|
|
852
|
-
headers,
|
|
853
|
-
models,
|
|
854
|
-
cacheFetchedAt,
|
|
855
|
-
refreshOnStart,
|
|
856
|
-
liveDiscoveryApiKey,
|
|
857
|
-
refreshInProgress: null,
|
|
731
|
+
baseUrl: normalizeBaseUrl(resolved.auth.baseUrl ?? requestBaseUrl(definition)),
|
|
732
|
+
apiKey: resolved.auth.apiKey,
|
|
733
|
+
headers: resolved.auth.headers,
|
|
858
734
|
};
|
|
859
735
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
let
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
// An alias must never inherit the default provider's env key; leaving the
|
|
870
|
-
// key unset makes requests fail loudly instead of leaking credentials.
|
|
871
|
-
return definition.apiKeyConfig;
|
|
872
|
-
}
|
|
873
|
-
function registerProvider(state, models = state.models, apiKeyConfig = state.creds.apiKeyConfig) {
|
|
874
|
-
const definition = state.definition;
|
|
875
|
-
pi.registerProvider(definition.name, {
|
|
876
|
-
name: definition.displayName,
|
|
877
|
-
baseUrl: state.creds.baseUrl ? `${state.creds.baseUrl}/v1` : "https://litellm.example.com/v1",
|
|
878
|
-
apiKey: apiKeyConfig ?? defaultApiKeyConfig(definition),
|
|
879
|
-
api: "openai-completions",
|
|
880
|
-
headers: escapeHeaderConfig(state.headers),
|
|
881
|
-
models,
|
|
882
|
-
refreshModels: ({ allowNetwork, credential, signal }) => allowNetwork && !discoveryDisabledReason()
|
|
883
|
-
? runRefresh(state, undefined, credential, signal).then((result) => result.models)
|
|
884
|
-
: Promise.resolve(state.models),
|
|
885
|
-
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)),
|
|
886
744
|
});
|
|
887
745
|
}
|
|
888
|
-
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"));
|
|
889
756
|
return {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
defaultState.cacheFetchedAt = next.fetchedAt;
|
|
894
|
-
const overridden = await applyOverrides(PROVIDER_NAME, next.models);
|
|
895
|
-
defaultState.models = overridden;
|
|
896
|
-
defaultState.creds = {
|
|
897
|
-
...defaultState.creds,
|
|
898
|
-
baseUrl: next.baseUrl,
|
|
899
|
-
apiKeyFingerprint: next.apiKeyFingerprint,
|
|
900
|
-
};
|
|
901
|
-
registerProvider(defaultState, overridden);
|
|
902
|
-
updateAllCosts();
|
|
903
|
-
},
|
|
757
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
758
|
+
apiKey,
|
|
759
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
904
760
|
};
|
|
905
761
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (!
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
});
|
|
921
792
|
}
|
|
922
|
-
function
|
|
923
|
-
if (!
|
|
793
|
+
async function registerMcpTools(auth, signal) {
|
|
794
|
+
if (!mcpEnabled || discoveryDisabledReason())
|
|
924
795
|
return;
|
|
925
|
-
|
|
926
|
-
|
|
796
|
+
const identity = mcpCatalogIdentity(auth);
|
|
797
|
+
while (mcpRegistration) {
|
|
798
|
+
await waitForMcpRegistration(mcpRegistration, signal);
|
|
799
|
+
signal?.throwIfAborted();
|
|
800
|
+
if (registeredMcpIdentity === identity)
|
|
801
|
+
return;
|
|
927
802
|
}
|
|
928
|
-
|
|
929
|
-
function seededRuntimeApiKey(state, seed) {
|
|
930
|
-
let first = seed;
|
|
931
|
-
return async () => {
|
|
932
|
-
if (first) {
|
|
933
|
-
const value = first;
|
|
934
|
-
first = undefined;
|
|
935
|
-
return value;
|
|
936
|
-
}
|
|
937
|
-
return resolveRuntimeApiKey(state);
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
async function registerMcpTools(state, onProgress) {
|
|
941
|
-
if (!mcpEnabled || !state.creds.baseUrl || !state.liveDiscoveryApiKey || discoveryDisabledReason())
|
|
803
|
+
if (registeredMcpIdentity === identity)
|
|
942
804
|
return;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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;
|
|
947
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;
|
|
948
823
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
registerSkillTools(defaultState);
|
|
954
|
-
async function refreshModelsAndCosts(state, onProgress, credential, signal) {
|
|
955
|
-
const fresh = credential ? credentialsFromRefresh(state, credential) : await resolveCredentials(state.definition);
|
|
956
|
-
const freshFp = fresh.apiKeyFingerprint;
|
|
957
|
-
if (!fresh.baseUrl || !fresh.apiKey || !freshFp) {
|
|
958
|
-
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;
|
|
959
827
|
}
|
|
960
|
-
const progress = onProgress
|
|
961
|
-
? (message) => onProgress(`LiteLLM: ${message}`)
|
|
962
|
-
: (message) => process.stderr.write(`LiteLLM: ${message}\n`);
|
|
963
|
-
const result = await discoverModels(fresh.baseUrl, fresh.apiKey, {
|
|
964
|
-
timeoutMs: getDiscoveryTimeoutMs(),
|
|
965
|
-
signal,
|
|
966
|
-
headers: state.headers,
|
|
967
|
-
onProgress: progress,
|
|
968
|
-
});
|
|
969
|
-
signal?.throwIfAborted();
|
|
970
|
-
const now = Date.now();
|
|
971
|
-
await writeCache(getCachePath(state.definition.name), {
|
|
972
|
-
baseUrl: fresh.baseUrl,
|
|
973
|
-
apiKeyFingerprint: freshFp,
|
|
974
|
-
headersFingerprint: computeHeadersFingerprint(state.headers),
|
|
975
|
-
fetchedAt: now,
|
|
976
|
-
source: result.source,
|
|
977
|
-
models: result.models,
|
|
978
|
-
});
|
|
979
|
-
const overridden = await applyOverrides(state.definition.name, result.models);
|
|
980
|
-
state.creds = fresh;
|
|
981
|
-
state.models = overridden;
|
|
982
|
-
state.liveDiscoveryApiKey = fresh.apiKey;
|
|
983
|
-
state.cacheFetchedAt = now;
|
|
984
|
-
state.refreshOnStart = false;
|
|
985
|
-
registerProvider(state, overridden, fresh.apiKeyConfig);
|
|
986
|
-
updateAllCosts();
|
|
987
|
-
if (state.definition.name === PROVIDER_NAME)
|
|
988
|
-
await registerMcpTools(state, onProgress);
|
|
989
|
-
return { providerName: state.definition.name, models: overridden, source: result.source };
|
|
990
828
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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` };
|
|
850
|
+
},
|
|
994
851
|
});
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const disabledReason = discoveryDisabledReason();
|
|
1001
|
-
if (disabledReason) {
|
|
1002
|
-
ctx.ui.notify(`LiteLLM refresh disabled (${disabledReason})`, "warning");
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const requestedProvider = args.trim();
|
|
1006
|
-
const statesToRefresh = requestedProvider
|
|
1007
|
-
? providerStates.filter((state) => state.definition.name === requestedProvider)
|
|
1008
|
-
: providerStates;
|
|
1009
|
-
if (statesToRefresh.length === 0) {
|
|
1010
|
-
ctx.ui.notify(`LiteLLM refresh failed: unknown provider ${requestedProvider}`, "error");
|
|
1011
|
-
return;
|
|
852
|
+
Object.assign(provider, { headers: resolveHeaders(definition) });
|
|
853
|
+
const refreshModels = provider.refreshModels;
|
|
854
|
+
provider.refreshModels = async (context) => {
|
|
855
|
+
try {
|
|
856
|
+
await refreshModels(context);
|
|
1012
857
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
});
|
|
1022
|
-
if (failed.length === 0) {
|
|
1023
|
-
if (succeeded.length === 1) {
|
|
1024
|
-
const result = succeeded[0];
|
|
1025
|
-
ctx.ui.notify(`LiteLLM: ${result.models.length} models refreshed (source: ${result.source})`, "info");
|
|
1026
|
-
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);
|
|
1027
866
|
}
|
|
1028
|
-
ctx.ui.notify(`LiteLLM: ${succeeded.length} providers refreshed (${succeeded
|
|
1029
|
-
.map((result) => `${result.providerName}: ${result.models.length} models`)
|
|
1030
|
-
.join(", ")})`, "info");
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const failures = failed.map(({ name, message }) => (settled.length === 1 ? message : `${name}: ${message}`));
|
|
1034
|
-
if (succeeded.length === 0) {
|
|
1035
|
-
ctx.ui.notify(`LiteLLM refresh failed: ${failures.join("; ")}`, "error");
|
|
1036
|
-
return;
|
|
1037
867
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
+
}
|
|
1043
877
|
let sessionId;
|
|
1044
878
|
pi.on("session_start", (_event, ctx) => {
|
|
1045
879
|
sessionId = getSessionIdFromFile(ctx.sessionManager.getSessionFile());
|
|
1046
|
-
if (discoveryDisabledReason())
|
|
1047
|
-
return;
|
|
1048
|
-
for (const state of providerStates) {
|
|
1049
|
-
const cacheIsStale = state.cacheFetchedAt > 0 && Date.now() - state.cacheFetchedAt > CACHE_STALE_MS;
|
|
1050
|
-
if (!state.refreshOnStart && !cacheIsStale)
|
|
1051
|
-
continue;
|
|
1052
|
-
const progress = ctx.ui?.notify ? (message) => ctx.ui.notify(message, "info") : undefined;
|
|
1053
|
-
void runRefresh(state, progress).catch(() => undefined);
|
|
1054
|
-
}
|
|
1055
880
|
});
|
|
1056
881
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1057
882
|
if (!ctx.model?.provider || !providerNames.has(ctx.model.provider))
|
|
@@ -1060,13 +885,15 @@ export default async function (pi) {
|
|
|
1060
885
|
return;
|
|
1061
886
|
return prepareLiteLLMRequestPayload(event.payload, ctx.model?.id, ctx.model?.api, sessionId);
|
|
1062
887
|
});
|
|
1063
|
-
pi.on("before_agent_start", async (event) => {
|
|
888
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
889
|
+
defaultRuntimeAuth = undefined;
|
|
1064
890
|
if (!skillsEnabled || discoveryDisabledReason())
|
|
1065
891
|
return;
|
|
1066
|
-
const
|
|
1067
|
-
if (!
|
|
892
|
+
const auth = await getRuntimeAuth(ctx);
|
|
893
|
+
if (!auth)
|
|
1068
894
|
return;
|
|
1069
|
-
|
|
895
|
+
defaultRuntimeAuth = auth;
|
|
896
|
+
const skills = await listSkills(defaultRuntimeAuth.baseUrl, defaultRuntimeAuth.apiKey, defaultRuntimeAuth.headers);
|
|
1070
897
|
const section = createSkillsPromptSection(skills);
|
|
1071
898
|
if (!section)
|
|
1072
899
|
return;
|