@acarmisc/backstage-plugin-litellm-backend 0.1.1 → 0.1.3
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/config.d.ts +68 -1
- package/dist/client.d.ts +9 -2
- package/dist/client.js +29 -6
- package/dist/index.cjs.js +157 -16
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +323 -0
- package/dist/index.esm.js.map +7 -0
- package/dist/plugin.js +2 -2
- package/dist/router.d.ts +2 -0
- package/dist/router.js +150 -8
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +46 -3
- package/package.json +1 -1
package/config.d.ts
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
export interface Config {
|
|
2
2
|
litellm: {
|
|
3
|
+
/**
|
|
4
|
+
* Base URL of the LiteLLM proxy instance.
|
|
5
|
+
* @visibility backend
|
|
6
|
+
*/
|
|
3
7
|
baseUrl: string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* LiteLLM master key for admin operations. Never exposed to the frontend.
|
|
11
|
+
* @visibility secret
|
|
12
|
+
*/
|
|
4
13
|
masterKey: string;
|
|
14
|
+
|
|
15
|
+
provisioning?: {
|
|
16
|
+
/**
|
|
17
|
+
* When true the backend automatically creates a LiteLLM user on first
|
|
18
|
+
* access if the Backstage user is not yet known to LiteLLM.
|
|
19
|
+
* Disabled by default — enable explicitly when you are ready.
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
enabled?: boolean;
|
|
23
|
+
|
|
24
|
+
defaults?: {
|
|
25
|
+
/**
|
|
26
|
+
* Max lifetime spend in USD before the account is blocked.
|
|
27
|
+
* Set a conservative value; null means no hard cap.
|
|
28
|
+
* @default 10
|
|
29
|
+
*/
|
|
30
|
+
maxBudget?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Spend-reset period for maxBudget (e.g. "30d", "7d", "1h").
|
|
34
|
+
* After this period the spend counter resets.
|
|
35
|
+
* @default "30d"
|
|
36
|
+
*/
|
|
37
|
+
budgetDuration?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* LiteLLM model IDs the new user is allowed to call.
|
|
41
|
+
* Empty array means all models configured in the proxy are allowed.
|
|
42
|
+
* @default []
|
|
43
|
+
*/
|
|
44
|
+
models?: string[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* LiteLLM team IDs to add the new user to automatically.
|
|
48
|
+
* The user inherits team-level model and budget restrictions.
|
|
49
|
+
* @default []
|
|
50
|
+
*/
|
|
51
|
+
teams?: string[];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tokens per minute hard cap across all models.
|
|
55
|
+
* Omit for no limit (team or global limits still apply).
|
|
56
|
+
*/
|
|
57
|
+
tpmLimit?: number;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Requests per minute hard cap across all models.
|
|
61
|
+
* Omit for no limit.
|
|
62
|
+
*/
|
|
63
|
+
rpmLimit?: number;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Arbitrary key-value metadata stored on the LiteLLM user record.
|
|
67
|
+
* Useful for tracking source, cost centre, department, etc.
|
|
68
|
+
*/
|
|
69
|
+
metadata?: Record<string, string>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
5
72
|
};
|
|
6
|
-
}
|
|
73
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } from './types';
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest, CreateUserRequest, CreateUserResponse } from './types';
|
|
2
2
|
export declare class LiteLLMClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private masterKey;
|
|
5
5
|
private timeout;
|
|
6
6
|
constructor(config: LiteLLMConfig, timeout?: number);
|
|
7
7
|
private request;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Returns null when the user is not found in LiteLLM (404).
|
|
10
|
+
* Throws on all other errors so callers know something went wrong.
|
|
11
|
+
*/
|
|
12
|
+
getUserInfo(userId?: string): Promise<UserInfo | null>;
|
|
13
|
+
createUser(payload: CreateUserRequest): Promise<CreateUserResponse>;
|
|
9
14
|
listKeys(userId?: string): Promise<VirtualKey[]>;
|
|
10
15
|
generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse>;
|
|
11
16
|
deleteKeys(request: DeleteKeyRequest): Promise<{
|
|
12
17
|
success: boolean;
|
|
13
18
|
}>;
|
|
14
19
|
listModels(): Promise<ModelInfo[]>;
|
|
20
|
+
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
15
21
|
getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics>;
|
|
22
|
+
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
16
23
|
}
|
package/dist/client.js
CHANGED
|
@@ -23,7 +23,9 @@ class LiteLLMClient {
|
|
|
23
23
|
});
|
|
24
24
|
if (!response.ok) {
|
|
25
25
|
const errorBody = await response.text();
|
|
26
|
-
|
|
26
|
+
const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
27
|
+
err.status = response.status;
|
|
28
|
+
throw err;
|
|
27
29
|
}
|
|
28
30
|
return response.json();
|
|
29
31
|
}
|
|
@@ -31,9 +33,26 @@ class LiteLLMClient {
|
|
|
31
33
|
clearTimeout(timeoutId);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns null when the user is not found in LiteLLM (404).
|
|
38
|
+
* Throws on all other errors so callers know something went wrong.
|
|
39
|
+
*/
|
|
34
40
|
async getUserInfo(userId) {
|
|
35
41
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
36
|
-
|
|
42
|
+
try {
|
|
43
|
+
return await this.request(`/user/info${query}`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err.status === 404)
|
|
47
|
+
return null;
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async createUser(payload) {
|
|
52
|
+
return this.request('/user/new', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: JSON.stringify(payload),
|
|
55
|
+
});
|
|
37
56
|
}
|
|
38
57
|
async listKeys(userId) {
|
|
39
58
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
@@ -56,16 +75,20 @@ class LiteLLMClient {
|
|
|
56
75
|
const response = await this.request('/models');
|
|
57
76
|
return Array.isArray(response) ? response : (response.data ?? []);
|
|
58
77
|
}
|
|
78
|
+
async getTeamInfo(teamId) {
|
|
79
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
80
|
+
}
|
|
59
81
|
async getUsage(startDate, endDate, userId, groupBy) {
|
|
60
|
-
const params = new URLSearchParams({
|
|
61
|
-
start_date: startDate,
|
|
62
|
-
end_date: endDate,
|
|
63
|
-
});
|
|
82
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
64
83
|
if (userId)
|
|
65
84
|
params.append('user_id', userId);
|
|
66
85
|
if (groupBy)
|
|
67
86
|
params.append('group_by', groupBy);
|
|
68
87
|
return this.request(`/usage/keys?${params.toString()}`);
|
|
69
88
|
}
|
|
89
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
90
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
91
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
92
|
+
}
|
|
70
93
|
}
|
|
71
94
|
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.cjs.js
CHANGED
|
@@ -55,16 +55,33 @@ var LiteLLMClient = class {
|
|
|
55
55
|
});
|
|
56
56
|
if (!response.ok) {
|
|
57
57
|
const errorBody = await response.text();
|
|
58
|
-
|
|
58
|
+
const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
59
|
+
err.status = response.status;
|
|
60
|
+
throw err;
|
|
59
61
|
}
|
|
60
62
|
return response.json();
|
|
61
63
|
} finally {
|
|
62
64
|
clearTimeout(timeoutId);
|
|
63
65
|
}
|
|
64
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns null when the user is not found in LiteLLM (404).
|
|
69
|
+
* Throws on all other errors so callers know something went wrong.
|
|
70
|
+
*/
|
|
65
71
|
async getUserInfo(userId) {
|
|
66
72
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
67
|
-
|
|
73
|
+
try {
|
|
74
|
+
return await this.request(`/user/info${query}`);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.status === 404) return null;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async createUser(payload) {
|
|
81
|
+
return this.request("/user/new", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify(payload)
|
|
84
|
+
});
|
|
68
85
|
}
|
|
69
86
|
async listKeys(userId) {
|
|
70
87
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
@@ -87,31 +104,106 @@ var LiteLLMClient = class {
|
|
|
87
104
|
const response = await this.request("/models");
|
|
88
105
|
return Array.isArray(response) ? response : response.data ?? [];
|
|
89
106
|
}
|
|
107
|
+
async getTeamInfo(teamId) {
|
|
108
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
109
|
+
}
|
|
90
110
|
async getUsage(startDate, endDate, userId, groupBy) {
|
|
91
|
-
const params = new URLSearchParams({
|
|
92
|
-
start_date: startDate,
|
|
93
|
-
end_date: endDate
|
|
94
|
-
});
|
|
111
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
95
112
|
if (userId) params.append("user_id", userId);
|
|
96
113
|
if (groupBy) params.append("group_by", groupBy);
|
|
97
114
|
return this.request(`/usage/keys?${params.toString()}`);
|
|
98
115
|
}
|
|
116
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
117
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
118
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
119
|
+
}
|
|
99
120
|
};
|
|
100
121
|
|
|
101
122
|
// src/router.ts
|
|
123
|
+
function readProvisioningDefaults(config) {
|
|
124
|
+
const enabled = config.getOptionalBoolean("litellm.provisioning.enabled") ?? false;
|
|
125
|
+
const defaults = {
|
|
126
|
+
maxBudget: config.getOptionalNumber("litellm.provisioning.defaults.maxBudget") ?? 10,
|
|
127
|
+
budgetDuration: config.getOptionalString("litellm.provisioning.defaults.budgetDuration") ?? "30d",
|
|
128
|
+
models: config.getOptionalStringArray("litellm.provisioning.defaults.models") ?? [],
|
|
129
|
+
teams: config.getOptionalStringArray("litellm.provisioning.defaults.teams") ?? [],
|
|
130
|
+
tpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.tpmLimit"),
|
|
131
|
+
rpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.rpmLimit"),
|
|
132
|
+
metadata: config.getOptional("litellm.provisioning.defaults.metadata") ?? {}
|
|
133
|
+
};
|
|
134
|
+
return { enabled, defaults };
|
|
135
|
+
}
|
|
136
|
+
async function resolveUserId(req, auth) {
|
|
137
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
138
|
+
if (!rawToken) return void 0;
|
|
139
|
+
try {
|
|
140
|
+
const credentials = await auth.authenticate(rawToken);
|
|
141
|
+
const principal = credentials.principal;
|
|
142
|
+
if (principal?.type === "user") {
|
|
143
|
+
return principal.userEntityRef;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
async function provisionUser(client, userId, defaults, logger) {
|
|
150
|
+
const payload = {
|
|
151
|
+
user_id: userId,
|
|
152
|
+
max_budget: defaults.maxBudget,
|
|
153
|
+
budget_duration: defaults.budgetDuration,
|
|
154
|
+
models: defaults.models,
|
|
155
|
+
teams: defaults.teams,
|
|
156
|
+
...defaults.tpmLimit !== void 0 && { tpm_limit: defaults.tpmLimit },
|
|
157
|
+
...defaults.rpmLimit !== void 0 && { rpm_limit: defaults.rpmLimit },
|
|
158
|
+
metadata: {
|
|
159
|
+
...defaults.metadata,
|
|
160
|
+
provisioned_by: "backstage",
|
|
161
|
+
provisioned_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
backstage_entity: userId
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);
|
|
166
|
+
try {
|
|
167
|
+
await client.createUser(payload);
|
|
168
|
+
return await client.getUserInfo(userId);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
102
174
|
async function createRouter(options) {
|
|
103
|
-
const { config, logger } = options;
|
|
175
|
+
const { config, logger, auth } = options;
|
|
104
176
|
const baseUrl = config.getString("litellm.baseUrl");
|
|
105
177
|
const masterKey = config.getString("litellm.masterKey");
|
|
106
178
|
const client = new LiteLLMClient({ baseUrl, masterKey });
|
|
179
|
+
const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
|
|
180
|
+
if (provisioningEnabled) {
|
|
181
|
+
logger.info(
|
|
182
|
+
`LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(",") : "all"}, teams=[${provisioningDefaults.teams.join(",")}]`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
107
185
|
const router = (0, import_express.Router)();
|
|
108
186
|
router.get("/health", (_req, res) => {
|
|
109
|
-
res.json({ status: "ok" });
|
|
187
|
+
res.json({ status: "ok", provisioning: provisioningEnabled });
|
|
110
188
|
});
|
|
111
189
|
router.get("/user/info", async (req, res) => {
|
|
112
190
|
try {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
191
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
192
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
193
|
+
let userInfo = await client.getUserInfo(userId);
|
|
194
|
+
if (!userInfo) {
|
|
195
|
+
if (provisioningEnabled && userId) {
|
|
196
|
+
userInfo = await provisionUser(client, userId, provisioningDefaults, logger);
|
|
197
|
+
}
|
|
198
|
+
if (!userInfo) {
|
|
199
|
+
res.status(404).json({
|
|
200
|
+
error: "User not found in LiteLLM",
|
|
201
|
+
provisioning: provisioningEnabled,
|
|
202
|
+
hint: provisioningEnabled ? "Provisioning attempted but failed \u2014 check LiteLLM logs" : "Enable litellm.provisioning.enabled in app-config.yaml or create the user manually"
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
115
207
|
res.json(userInfo);
|
|
116
208
|
} catch (error) {
|
|
117
209
|
logger.error("Failed to fetch user info", error);
|
|
@@ -120,7 +212,8 @@ async function createRouter(options) {
|
|
|
120
212
|
});
|
|
121
213
|
router.get("/keys", async (req, res) => {
|
|
122
214
|
try {
|
|
123
|
-
const
|
|
215
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
216
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
124
217
|
const keys = await client.listKeys(userId);
|
|
125
218
|
res.json(keys);
|
|
126
219
|
} catch (error) {
|
|
@@ -130,7 +223,11 @@ async function createRouter(options) {
|
|
|
130
223
|
});
|
|
131
224
|
router.post("/keys/generate", async (req, res) => {
|
|
132
225
|
try {
|
|
133
|
-
const
|
|
226
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
227
|
+
const request = {
|
|
228
|
+
...req.body,
|
|
229
|
+
...tokenUserId && { user_id: tokenUserId }
|
|
230
|
+
};
|
|
134
231
|
const result = await client.generateKey(request);
|
|
135
232
|
res.json(result);
|
|
136
233
|
} catch (error) {
|
|
@@ -161,17 +258,61 @@ async function createRouter(options) {
|
|
|
161
258
|
res.status(500).json({ error: error.message });
|
|
162
259
|
}
|
|
163
260
|
});
|
|
261
|
+
router.get("/teams", async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
264
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
265
|
+
const userInfo = await client.getUserInfo(userId);
|
|
266
|
+
if (!userInfo?.teams?.length) {
|
|
267
|
+
res.json([]);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const teams = await Promise.all(
|
|
271
|
+
userInfo.teams.map(
|
|
272
|
+
(teamId) => client.getTeamInfo(teamId).catch((err) => {
|
|
273
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
274
|
+
return null;
|
|
275
|
+
})
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
res.json(teams.filter(Boolean));
|
|
279
|
+
} catch (error) {
|
|
280
|
+
logger.error("Failed to fetch teams", error);
|
|
281
|
+
res.status(500).json({ error: error.message });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
router.get("/teams/:teamId/usage", async (req, res) => {
|
|
285
|
+
try {
|
|
286
|
+
const { teamId } = req.params;
|
|
287
|
+
const { start_date, end_date } = req.query;
|
|
288
|
+
if (!start_date || !end_date) {
|
|
289
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const usage = await client.getTeamUsage(
|
|
293
|
+
teamId,
|
|
294
|
+
start_date,
|
|
295
|
+
end_date
|
|
296
|
+
);
|
|
297
|
+
res.json(usage);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger.error("Failed to fetch team usage", error);
|
|
300
|
+
res.status(500).json({ error: error.message });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
164
303
|
router.get("/usage", async (req, res) => {
|
|
165
304
|
try {
|
|
166
|
-
const { start_date, end_date,
|
|
305
|
+
const { start_date, end_date, group_by } = req.query;
|
|
167
306
|
if (!start_date || !end_date) {
|
|
168
307
|
res.status(400).json({ error: "start_date and end_date are required" });
|
|
169
308
|
return;
|
|
170
309
|
}
|
|
310
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
311
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
171
312
|
const usage = await client.getUsage(
|
|
172
313
|
start_date,
|
|
173
314
|
end_date,
|
|
174
|
-
|
|
315
|
+
userId,
|
|
175
316
|
group_by
|
|
176
317
|
);
|
|
177
318
|
res.json(usage);
|
|
@@ -195,8 +336,8 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
195
336
|
auth: import_backend_plugin_api.coreServices.auth,
|
|
196
337
|
discovery: import_backend_plugin_api.coreServices.discovery
|
|
197
338
|
},
|
|
198
|
-
async init({ httpRouter, config, logger }) {
|
|
199
|
-
const router = await createRouter({ config, logger });
|
|
339
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
340
|
+
const router = await createRouter({ config, logger, auth });
|
|
200
341
|
httpRouter.use(router);
|
|
201
342
|
}
|
|
202
343
|
});
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts", "../src/plugin.ts", "../src/router.ts", "../src/client.ts"],
|
|
4
|
-
"sourcesContent": ["export { litellmPlugin } from './plugin';\nexport { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';", "import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger }) {\n const router = await createRouter({ config, logger });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n GenerateKeyRequest,\n GenerateKeyResponse,\n} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/user/info', async (req: Request, res: Response) => {\n try {\n const userId = req.query.user_id as string | undefined;\n const userInfo: UserInfo = await client.getUserInfo(userId);\n res.json(userInfo);\n } catch (error: any) {\n logger.error('Failed to fetch user info', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/keys', async (req: Request, res: Response) => {\n try {\n const userId = req.query.user_id as string | undefined;\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n logger.error('Failed to list keys', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/generate', async (req: Request, res: Response) => {\n try {\n const request: GenerateKeyRequest = req.body;\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.delete('/keys/:keyId', async (req: Request, res: Response) => {\n try {\n const { keyId } = req.params;\n if (!keyId) {\n res.status(400).json({ error: 'keyId is required' });\n return;\n }\n await client.deleteKeys({ keys: [keyId] });\n res.json({ success: true });\n } catch (error: any) {\n logger.error('Failed to delete key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/models', async (_req: Request, res: Response) => {\n try {\n const models: ModelInfo[] = await client.listModels();\n res.json(models);\n } catch (error: any) {\n logger.error('Failed to list models', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/usage', async (req: Request, res: Response) => {\n try {\n const { start_date, end_date, user_id, group_by } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const usage: UsageMetrics = await client.getUsage(\n start_date as string,\n end_date as string,\n user_id as string | undefined,\n group_by as string | undefined\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n return router;\n}", "import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } from './types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\nexport class LiteLLMClient {\n private baseUrl: string;\n private masterKey: string;\n private timeout: number;\n\n constructor(config: LiteLLMConfig, timeout = DEFAULT_TIMEOUT) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.masterKey = config.masterKey;\n this.timeout = timeout;\n }\n\n private async request<T>(path: string, options: RequestInit = {}): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(`${this.baseUrl}${path}`, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.masterKey}`,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);\n }\n\n return response.json();\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async getUserInfo(userId?: string): Promise<UserInfo> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n return this.request<UserInfo>(`/user/info${query}`);\n }\n\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async deleteKeys(request: DeleteKeyRequest): Promise<{ success: boolean }> {\n return this.request<{ success: boolean }>('/key/delete', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async listModels(): Promise<ModelInfo[]> {\n const response = await this.request<{ data: ModelInfo[] } | ModelInfo[]>('/models');\n return Array.isArray(response) ? response : (response.data ?? []);\n }\n\n async getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n });\n if (userId) params.append('user_id', userId);\n if (groupBy) params.append('group_by', groupBy);\n\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,gCAAkD;;;ACAlD,qBAA0C;;;
|
|
4
|
+
"sourcesContent": ["export { litellmPlugin } from './plugin';\nexport { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';", "import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger, auth }) {\n const router = await createRouter({ config, logger, auth });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n ProvisioningDefaults,\n} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\n}\n\n/**\n * Reads the provisioning block from config, applying safe defaults for every\n * field so the feature works out-of-the-box without any YAML required.\n *\n * Safe defaults rationale:\n * maxBudget: $10 \u2014 prevents runaway spend on a forgotten test account\n * budgetDuration: 30d \u2014 monthly reset, aligns with typical billing cycles\n * models: [] \u2014 empty means all proxy models are allowed;\n * restrict here or at team level for tighter control\n * teams: [] \u2014 no automatic team assignment; add IDs to enrol users\n * tpmLimit: none \u2014 LiteLLM global / team limits still apply\n * rpmLimit: none \u2014 same\n * metadata: backstage source tag only\n */\nfunction readProvisioningDefaults(config: Config): { enabled: boolean; defaults: ProvisioningDefaults } {\n const enabled = config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;\n const defaults: ProvisioningDefaults = {\n maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,\n budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',\n models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ?? [],\n teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ?? [],\n tpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.tpmLimit'),\n rpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.rpmLimit'),\n metadata: (config.getOptional<Record<string, string>>('litellm.provisioning.defaults.metadata') ?? {}),\n };\n return { enabled, defaults };\n}\n\n/**\n * Extracts the authenticated Backstage user identity from the request token.\n * Returns the userEntityRef (e.g. \"user:default/john.doe\") or undefined when\n * the request carries no user credential (service-to-service calls).\n */\nasync function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined> {\n const rawToken = req.headers.authorization?.slice(7);\n if (!rawToken) return undefined;\n try {\n const credentials = await auth.authenticate(rawToken);\n const principal = credentials.principal as any;\n if (principal?.type === 'user') {\n return principal.userEntityRef as string;\n }\n } catch {\n // invalid or service token \u2014 caller gets query-param fallback\n }\n return undefined;\n}\n\n/**\n * Creates a LiteLLM user for the given Backstage identity using the configured\n * defaults. Returns the UserInfo of the newly created account.\n */\nasync function provisionUser(\n client: LiteLLMClient,\n userId: string,\n defaults: ProvisioningDefaults,\n logger: any,\n): Promise<UserInfo | null> {\n const payload = {\n user_id: userId,\n max_budget: defaults.maxBudget,\n budget_duration: defaults.budgetDuration,\n models: defaults.models,\n teams: defaults.teams,\n ...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),\n ...(defaults.rpmLimit !== undefined && { rpm_limit: defaults.rpmLimit }),\n metadata: {\n ...defaults.metadata,\n provisioned_by: 'backstage',\n provisioned_at: new Date().toISOString(),\n backstage_entity: userId,\n },\n };\n\n logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);\n try {\n await client.createUser(payload);\n // Fetch the freshly-created user record to return consistent UserInfo shape\n return await client.getUserInfo(userId);\n } catch (err: any) {\n logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);\n return null;\n }\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger, auth } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);\n\n if (provisioningEnabled) {\n logger.info(\n `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`,\n );\n }\n\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok', provisioning: provisioningEnabled });\n });\n\n router.get('/user/info', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n\n let userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo) {\n if (provisioningEnabled && userId) {\n userInfo = await provisionUser(client, userId, provisioningDefaults, logger);\n }\n\n if (!userInfo) {\n res.status(404).json({\n error: 'User not found in LiteLLM',\n provisioning: provisioningEnabled,\n hint: provisioningEnabled\n ? 'Provisioning attempted but failed \u2014 check LiteLLM logs'\n : 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',\n });\n return;\n }\n }\n\n res.json(userInfo);\n } catch (error: any) {\n logger.error('Failed to fetch user info', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/keys', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n logger.error('Failed to list keys', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/generate', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const request: GenerateKeyRequest = {\n ...req.body,\n ...(tokenUserId && { user_id: tokenUserId }),\n };\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.delete('/keys/:keyId', async (req: Request, res: Response) => {\n try {\n const { keyId } = req.params;\n if (!keyId) {\n res.status(400).json({ error: 'keyId is required' });\n return;\n }\n await client.deleteKeys({ keys: [keyId] });\n res.json({ success: true });\n } catch (error: any) {\n logger.error('Failed to delete key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/models', async (_req: Request, res: Response) => {\n try {\n const models: ModelInfo[] = await client.listModels();\n res.json(models);\n } catch (error: any) {\n logger.error('Failed to list models', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo?.teams?.length) {\n res.json([]);\n return;\n }\n\n const teams = await Promise.all(\n userInfo.teams.map(teamId =>\n client.getTeamInfo(teamId).catch(err => {\n logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);\n return null;\n }),\n ),\n );\n res.json(teams.filter(Boolean) as TeamInfo[]);\n } catch (error: any) {\n logger.error('Failed to fetch teams', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams/:teamId/usage', async (req: Request, res: Response) => {\n try {\n const { teamId } = req.params;\n const { start_date, end_date } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const usage: UsageMetrics = await client.getTeamUsage(\n teamId,\n start_date as string,\n end_date as string,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch team usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/usage', async (req: Request, res: Response) => {\n try {\n const { start_date, end_date, group_by } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const usage: UsageMetrics = await client.getUsage(\n start_date as string,\n end_date as string,\n userId,\n group_by as string | undefined,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n return router;\n}\n", "import {\n LiteLLMConfig,\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n DeleteKeyRequest,\n CreateUserRequest,\n CreateUserResponse,\n} from './types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\nexport class LiteLLMClient {\n private baseUrl: string;\n private masterKey: string;\n private timeout: number;\n\n constructor(config: LiteLLMConfig, timeout = DEFAULT_TIMEOUT) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.masterKey = config.masterKey;\n this.timeout = timeout;\n }\n\n private async request<T>(path: string, options: RequestInit = {}): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(`${this.baseUrl}${path}`, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.masterKey}`,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);\n (err as any).status = response.status;\n throw err;\n }\n\n return response.json();\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Returns null when the user is not found in LiteLLM (404).\n * Throws on all other errors so callers know something went wrong.\n */\n async getUserInfo(userId?: string): Promise<UserInfo | null> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n try {\n return await this.request<UserInfo>(`/user/info${query}`);\n } catch (err: any) {\n if (err.status === 404) return null;\n throw err;\n }\n }\n\n async createUser(payload: CreateUserRequest): Promise<CreateUserResponse> {\n return this.request<CreateUserResponse>('/user/new', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async deleteKeys(request: DeleteKeyRequest): Promise<{ success: boolean }> {\n return this.request<{ success: boolean }>('/key/delete', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async listModels(): Promise<ModelInfo[]> {\n const response = await this.request<{ data: ModelInfo[] } | ModelInfo[]>('/models');\n return Array.isArray(response) ? response : (response.data ?? []);\n }\n\n async getTeamInfo(teamId: string): Promise<TeamInfo> {\n return this.request<TeamInfo>(`/team/info?team_id=${encodeURIComponent(teamId)}`);\n }\n\n async getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate });\n if (userId) params.append('user_id', userId);\n if (groupBy) params.append('group_by', groupBy);\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n\n async getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { coreServices, createBackendPlugin } from "@backstage/backend-plugin-api";
|
|
3
|
+
|
|
4
|
+
// src/router.ts
|
|
5
|
+
import { Router } from "express";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
9
|
+
var LiteLLMClient = class {
|
|
10
|
+
constructor(config, timeout = DEFAULT_TIMEOUT) {
|
|
11
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
12
|
+
this.masterKey = config.masterKey;
|
|
13
|
+
this.timeout = timeout;
|
|
14
|
+
}
|
|
15
|
+
async request(path, options = {}) {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
20
|
+
...options,
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"Authorization": `Bearer ${this.masterKey}`,
|
|
25
|
+
...options.headers
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorBody = await response.text();
|
|
30
|
+
const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
31
|
+
err.status = response.status;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return response.json();
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns null when the user is not found in LiteLLM (404).
|
|
41
|
+
* Throws on all other errors so callers know something went wrong.
|
|
42
|
+
*/
|
|
43
|
+
async getUserInfo(userId) {
|
|
44
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
45
|
+
try {
|
|
46
|
+
return await this.request(`/user/info${query}`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.status === 404) return null;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async createUser(payload) {
|
|
53
|
+
return this.request("/user/new", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: JSON.stringify(payload)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async listKeys(userId) {
|
|
59
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
60
|
+
const response = await this.request(`/key/info${query}`);
|
|
61
|
+
return Array.isArray(response) ? response : response.info ?? [];
|
|
62
|
+
}
|
|
63
|
+
async generateKey(request) {
|
|
64
|
+
return this.request("/key/generate", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify(request)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async deleteKeys(request) {
|
|
70
|
+
return this.request("/key/delete", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
body: JSON.stringify(request)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async listModels() {
|
|
76
|
+
const response = await this.request("/models");
|
|
77
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
78
|
+
}
|
|
79
|
+
async getTeamInfo(teamId) {
|
|
80
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
81
|
+
}
|
|
82
|
+
async getUsage(startDate, endDate, userId, groupBy) {
|
|
83
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
84
|
+
if (userId) params.append("user_id", userId);
|
|
85
|
+
if (groupBy) params.append("group_by", groupBy);
|
|
86
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
87
|
+
}
|
|
88
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
89
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
90
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/router.ts
|
|
95
|
+
function readProvisioningDefaults(config) {
|
|
96
|
+
const enabled = config.getOptionalBoolean("litellm.provisioning.enabled") ?? false;
|
|
97
|
+
const defaults = {
|
|
98
|
+
maxBudget: config.getOptionalNumber("litellm.provisioning.defaults.maxBudget") ?? 10,
|
|
99
|
+
budgetDuration: config.getOptionalString("litellm.provisioning.defaults.budgetDuration") ?? "30d",
|
|
100
|
+
models: config.getOptionalStringArray("litellm.provisioning.defaults.models") ?? [],
|
|
101
|
+
teams: config.getOptionalStringArray("litellm.provisioning.defaults.teams") ?? [],
|
|
102
|
+
tpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.tpmLimit"),
|
|
103
|
+
rpmLimit: config.getOptionalNumber("litellm.provisioning.defaults.rpmLimit"),
|
|
104
|
+
metadata: config.getOptional("litellm.provisioning.defaults.metadata") ?? {}
|
|
105
|
+
};
|
|
106
|
+
return { enabled, defaults };
|
|
107
|
+
}
|
|
108
|
+
async function resolveUserId(req, auth) {
|
|
109
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
110
|
+
if (!rawToken) return void 0;
|
|
111
|
+
try {
|
|
112
|
+
const credentials = await auth.authenticate(rawToken);
|
|
113
|
+
const principal = credentials.principal;
|
|
114
|
+
if (principal?.type === "user") {
|
|
115
|
+
return principal.userEntityRef;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
async function provisionUser(client, userId, defaults, logger) {
|
|
122
|
+
const payload = {
|
|
123
|
+
user_id: userId,
|
|
124
|
+
max_budget: defaults.maxBudget,
|
|
125
|
+
budget_duration: defaults.budgetDuration,
|
|
126
|
+
models: defaults.models,
|
|
127
|
+
teams: defaults.teams,
|
|
128
|
+
...defaults.tpmLimit !== void 0 && { tpm_limit: defaults.tpmLimit },
|
|
129
|
+
...defaults.rpmLimit !== void 0 && { rpm_limit: defaults.rpmLimit },
|
|
130
|
+
metadata: {
|
|
131
|
+
...defaults.metadata,
|
|
132
|
+
provisioned_by: "backstage",
|
|
133
|
+
provisioned_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
backstage_entity: userId
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);
|
|
138
|
+
try {
|
|
139
|
+
await client.createUser(payload);
|
|
140
|
+
return await client.getUserInfo(userId);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function createRouter(options) {
|
|
147
|
+
const { config, logger, auth } = options;
|
|
148
|
+
const baseUrl = config.getString("litellm.baseUrl");
|
|
149
|
+
const masterKey = config.getString("litellm.masterKey");
|
|
150
|
+
const client = new LiteLLMClient({ baseUrl, masterKey });
|
|
151
|
+
const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
|
|
152
|
+
if (provisioningEnabled) {
|
|
153
|
+
logger.info(
|
|
154
|
+
`LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(",") : "all"}, teams=[${provisioningDefaults.teams.join(",")}]`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const router = Router();
|
|
158
|
+
router.get("/health", (_req, res) => {
|
|
159
|
+
res.json({ status: "ok", provisioning: provisioningEnabled });
|
|
160
|
+
});
|
|
161
|
+
router.get("/user/info", async (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
164
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
165
|
+
let userInfo = await client.getUserInfo(userId);
|
|
166
|
+
if (!userInfo) {
|
|
167
|
+
if (provisioningEnabled && userId) {
|
|
168
|
+
userInfo = await provisionUser(client, userId, provisioningDefaults, logger);
|
|
169
|
+
}
|
|
170
|
+
if (!userInfo) {
|
|
171
|
+
res.status(404).json({
|
|
172
|
+
error: "User not found in LiteLLM",
|
|
173
|
+
provisioning: provisioningEnabled,
|
|
174
|
+
hint: provisioningEnabled ? "Provisioning attempted but failed \u2014 check LiteLLM logs" : "Enable litellm.provisioning.enabled in app-config.yaml or create the user manually"
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
res.json(userInfo);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
logger.error("Failed to fetch user info", error);
|
|
182
|
+
res.status(500).json({ error: error.message });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
router.get("/keys", async (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
188
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
189
|
+
const keys = await client.listKeys(userId);
|
|
190
|
+
res.json(keys);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error("Failed to list keys", error);
|
|
193
|
+
res.status(500).json({ error: error.message });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
router.post("/keys/generate", async (req, res) => {
|
|
197
|
+
try {
|
|
198
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
199
|
+
const request = {
|
|
200
|
+
...req.body,
|
|
201
|
+
...tokenUserId && { user_id: tokenUserId }
|
|
202
|
+
};
|
|
203
|
+
const result = await client.generateKey(request);
|
|
204
|
+
res.json(result);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.error("Failed to generate key", error);
|
|
207
|
+
res.status(500).json({ error: error.message });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
router.delete("/keys/:keyId", async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const { keyId } = req.params;
|
|
213
|
+
if (!keyId) {
|
|
214
|
+
res.status(400).json({ error: "keyId is required" });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await client.deleteKeys({ keys: [keyId] });
|
|
218
|
+
res.json({ success: true });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.error("Failed to delete key", error);
|
|
221
|
+
res.status(500).json({ error: error.message });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
router.get("/models", async (_req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
const models = await client.listModels();
|
|
227
|
+
res.json(models);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logger.error("Failed to list models", error);
|
|
230
|
+
res.status(500).json({ error: error.message });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
router.get("/teams", async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
236
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
237
|
+
const userInfo = await client.getUserInfo(userId);
|
|
238
|
+
if (!userInfo?.teams?.length) {
|
|
239
|
+
res.json([]);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const teams = await Promise.all(
|
|
243
|
+
userInfo.teams.map(
|
|
244
|
+
(teamId) => client.getTeamInfo(teamId).catch((err) => {
|
|
245
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
246
|
+
return null;
|
|
247
|
+
})
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
res.json(teams.filter(Boolean));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error("Failed to fetch teams", error);
|
|
253
|
+
res.status(500).json({ error: error.message });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
router.get("/teams/:teamId/usage", async (req, res) => {
|
|
257
|
+
try {
|
|
258
|
+
const { teamId } = req.params;
|
|
259
|
+
const { start_date, end_date } = req.query;
|
|
260
|
+
if (!start_date || !end_date) {
|
|
261
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const usage = await client.getTeamUsage(
|
|
265
|
+
teamId,
|
|
266
|
+
start_date,
|
|
267
|
+
end_date
|
|
268
|
+
);
|
|
269
|
+
res.json(usage);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
logger.error("Failed to fetch team usage", error);
|
|
272
|
+
res.status(500).json({ error: error.message });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
router.get("/usage", async (req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
const { start_date, end_date, group_by } = req.query;
|
|
278
|
+
if (!start_date || !end_date) {
|
|
279
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
283
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
284
|
+
const usage = await client.getUsage(
|
|
285
|
+
start_date,
|
|
286
|
+
end_date,
|
|
287
|
+
userId,
|
|
288
|
+
group_by
|
|
289
|
+
);
|
|
290
|
+
res.json(usage);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error("Failed to fetch usage", error);
|
|
293
|
+
res.status(500).json({ error: error.message });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return router;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/plugin.ts
|
|
300
|
+
var litellmPlugin = createBackendPlugin({
|
|
301
|
+
pluginId: "litellm",
|
|
302
|
+
register(reg) {
|
|
303
|
+
reg.registerInit({
|
|
304
|
+
deps: {
|
|
305
|
+
httpRouter: coreServices.httpRouter,
|
|
306
|
+
config: coreServices.rootConfig,
|
|
307
|
+
logger: coreServices.logger,
|
|
308
|
+
auth: coreServices.auth,
|
|
309
|
+
discovery: coreServices.discovery
|
|
310
|
+
},
|
|
311
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
312
|
+
const router = await createRouter({ config, logger, auth });
|
|
313
|
+
httpRouter.use(router);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
export {
|
|
319
|
+
LiteLLMClient,
|
|
320
|
+
createRouter,
|
|
321
|
+
litellmPlugin
|
|
322
|
+
};
|
|
323
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/plugin.ts", "../src/router.ts", "../src/client.ts"],
|
|
4
|
+
"sourcesContent": ["import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger, auth }) {\n const router = await createRouter({ config, logger, auth });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n ProvisioningDefaults,\n} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\n}\n\n/**\n * Reads the provisioning block from config, applying safe defaults for every\n * field so the feature works out-of-the-box without any YAML required.\n *\n * Safe defaults rationale:\n * maxBudget: $10 \u2014 prevents runaway spend on a forgotten test account\n * budgetDuration: 30d \u2014 monthly reset, aligns with typical billing cycles\n * models: [] \u2014 empty means all proxy models are allowed;\n * restrict here or at team level for tighter control\n * teams: [] \u2014 no automatic team assignment; add IDs to enrol users\n * tpmLimit: none \u2014 LiteLLM global / team limits still apply\n * rpmLimit: none \u2014 same\n * metadata: backstage source tag only\n */\nfunction readProvisioningDefaults(config: Config): { enabled: boolean; defaults: ProvisioningDefaults } {\n const enabled = config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;\n const defaults: ProvisioningDefaults = {\n maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,\n budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',\n models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ?? [],\n teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ?? [],\n tpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.tpmLimit'),\n rpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.rpmLimit'),\n metadata: (config.getOptional<Record<string, string>>('litellm.provisioning.defaults.metadata') ?? {}),\n };\n return { enabled, defaults };\n}\n\n/**\n * Extracts the authenticated Backstage user identity from the request token.\n * Returns the userEntityRef (e.g. \"user:default/john.doe\") or undefined when\n * the request carries no user credential (service-to-service calls).\n */\nasync function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined> {\n const rawToken = req.headers.authorization?.slice(7);\n if (!rawToken) return undefined;\n try {\n const credentials = await auth.authenticate(rawToken);\n const principal = credentials.principal as any;\n if (principal?.type === 'user') {\n return principal.userEntityRef as string;\n }\n } catch {\n // invalid or service token \u2014 caller gets query-param fallback\n }\n return undefined;\n}\n\n/**\n * Creates a LiteLLM user for the given Backstage identity using the configured\n * defaults. Returns the UserInfo of the newly created account.\n */\nasync function provisionUser(\n client: LiteLLMClient,\n userId: string,\n defaults: ProvisioningDefaults,\n logger: any,\n): Promise<UserInfo | null> {\n const payload = {\n user_id: userId,\n max_budget: defaults.maxBudget,\n budget_duration: defaults.budgetDuration,\n models: defaults.models,\n teams: defaults.teams,\n ...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),\n ...(defaults.rpmLimit !== undefined && { rpm_limit: defaults.rpmLimit }),\n metadata: {\n ...defaults.metadata,\n provisioned_by: 'backstage',\n provisioned_at: new Date().toISOString(),\n backstage_entity: userId,\n },\n };\n\n logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);\n try {\n await client.createUser(payload);\n // Fetch the freshly-created user record to return consistent UserInfo shape\n return await client.getUserInfo(userId);\n } catch (err: any) {\n logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);\n return null;\n }\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger, auth } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);\n\n if (provisioningEnabled) {\n logger.info(\n `LiteLLM auto-provisioning enabled \u2014 defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`,\n );\n }\n\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok', provisioning: provisioningEnabled });\n });\n\n router.get('/user/info', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n\n let userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo) {\n if (provisioningEnabled && userId) {\n userInfo = await provisionUser(client, userId, provisioningDefaults, logger);\n }\n\n if (!userInfo) {\n res.status(404).json({\n error: 'User not found in LiteLLM',\n provisioning: provisioningEnabled,\n hint: provisioningEnabled\n ? 'Provisioning attempted but failed \u2014 check LiteLLM logs'\n : 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',\n });\n return;\n }\n }\n\n res.json(userInfo);\n } catch (error: any) {\n logger.error('Failed to fetch user info', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/keys', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n logger.error('Failed to list keys', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/generate', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const request: GenerateKeyRequest = {\n ...req.body,\n ...(tokenUserId && { user_id: tokenUserId }),\n };\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.delete('/keys/:keyId', async (req: Request, res: Response) => {\n try {\n const { keyId } = req.params;\n if (!keyId) {\n res.status(400).json({ error: 'keyId is required' });\n return;\n }\n await client.deleteKeys({ keys: [keyId] });\n res.json({ success: true });\n } catch (error: any) {\n logger.error('Failed to delete key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/models', async (_req: Request, res: Response) => {\n try {\n const models: ModelInfo[] = await client.listModels();\n res.json(models);\n } catch (error: any) {\n logger.error('Failed to list models', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams', async (req: Request, res: Response) => {\n try {\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const userInfo: UserInfo | null = await client.getUserInfo(userId);\n\n if (!userInfo?.teams?.length) {\n res.json([]);\n return;\n }\n\n const teams = await Promise.all(\n userInfo.teams.map(teamId =>\n client.getTeamInfo(teamId).catch(err => {\n logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);\n return null;\n }),\n ),\n );\n res.json(teams.filter(Boolean) as TeamInfo[]);\n } catch (error: any) {\n logger.error('Failed to fetch teams', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/teams/:teamId/usage', async (req: Request, res: Response) => {\n try {\n const { teamId } = req.params;\n const { start_date, end_date } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const usage: UsageMetrics = await client.getTeamUsage(\n teamId,\n start_date as string,\n end_date as string,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch team usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/usage', async (req: Request, res: Response) => {\n try {\n const { start_date, end_date, group_by } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (req.query.user_id as string | undefined);\n const usage: UsageMetrics = await client.getUsage(\n start_date as string,\n end_date as string,\n userId,\n group_by as string | undefined,\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n return router;\n}\n", "import {\n LiteLLMConfig,\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n DeleteKeyRequest,\n CreateUserRequest,\n CreateUserResponse,\n} from './types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\nexport class LiteLLMClient {\n private baseUrl: string;\n private masterKey: string;\n private timeout: number;\n\n constructor(config: LiteLLMConfig, timeout = DEFAULT_TIMEOUT) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.masterKey = config.masterKey;\n this.timeout = timeout;\n }\n\n private async request<T>(path: string, options: RequestInit = {}): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(`${this.baseUrl}${path}`, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.masterKey}`,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);\n (err as any).status = response.status;\n throw err;\n }\n\n return response.json();\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n /**\n * Returns null when the user is not found in LiteLLM (404).\n * Throws on all other errors so callers know something went wrong.\n */\n async getUserInfo(userId?: string): Promise<UserInfo | null> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n try {\n return await this.request<UserInfo>(`/user/info${query}`);\n } catch (err: any) {\n if (err.status === 404) return null;\n throw err;\n }\n }\n\n async createUser(payload: CreateUserRequest): Promise<CreateUserResponse> {\n return this.request<CreateUserResponse>('/user/new', {\n method: 'POST',\n body: JSON.stringify(payload),\n });\n }\n\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async deleteKeys(request: DeleteKeyRequest): Promise<{ success: boolean }> {\n return this.request<{ success: boolean }>('/key/delete', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async listModels(): Promise<ModelInfo[]> {\n const response = await this.request<{ data: ModelInfo[] } | ModelInfo[]>('/models');\n return Array.isArray(response) ? response : (response.data ?? []);\n }\n\n async getTeamInfo(teamId: string): Promise<TeamInfo> {\n return this.request<TeamInfo>(`/team/info?team_id=${encodeURIComponent(teamId)}`);\n }\n\n async getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate });\n if (userId) params.append('user_id', userId);\n if (groupBy) params.append('group_by', groupBy);\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n\n async getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -14,8 +14,8 @@ exports.litellmPlugin = (0, backend_plugin_api_1.createBackendPlugin)({
|
|
|
14
14
|
auth: backend_plugin_api_1.coreServices.auth,
|
|
15
15
|
discovery: backend_plugin_api_1.coreServices.discovery,
|
|
16
16
|
},
|
|
17
|
-
async init({ httpRouter, config, logger }) {
|
|
18
|
-
const router = await (0, router_1.createRouter)({ config, logger });
|
|
17
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
18
|
+
const router = await (0, router_1.createRouter)({ config, logger, auth });
|
|
19
19
|
httpRouter.use(router);
|
|
20
20
|
},
|
|
21
21
|
});
|
package/dist/router.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
|
+
import { AuthService } from '@backstage/backend-plugin-api';
|
|
3
4
|
export interface RouterOptions {
|
|
4
5
|
config: Config;
|
|
5
6
|
logger: any;
|
|
7
|
+
auth: AuthService;
|
|
6
8
|
}
|
|
7
9
|
export declare function createRouter(options: RouterOptions): Promise<Router>;
|
package/dist/router.js
CHANGED
|
@@ -3,19 +3,118 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createRouter = createRouter;
|
|
4
4
|
const express_1 = require("express");
|
|
5
5
|
const client_1 = require("./client");
|
|
6
|
+
/**
|
|
7
|
+
* Reads the provisioning block from config, applying safe defaults for every
|
|
8
|
+
* field so the feature works out-of-the-box without any YAML required.
|
|
9
|
+
*
|
|
10
|
+
* Safe defaults rationale:
|
|
11
|
+
* maxBudget: $10 — prevents runaway spend on a forgotten test account
|
|
12
|
+
* budgetDuration: 30d — monthly reset, aligns with typical billing cycles
|
|
13
|
+
* models: [] — empty means all proxy models are allowed;
|
|
14
|
+
* restrict here or at team level for tighter control
|
|
15
|
+
* teams: [] — no automatic team assignment; add IDs to enrol users
|
|
16
|
+
* tpmLimit: none — LiteLLM global / team limits still apply
|
|
17
|
+
* rpmLimit: none — same
|
|
18
|
+
* metadata: backstage source tag only
|
|
19
|
+
*/
|
|
20
|
+
function readProvisioningDefaults(config) {
|
|
21
|
+
const enabled = config.getOptionalBoolean('litellm.provisioning.enabled') ?? false;
|
|
22
|
+
const defaults = {
|
|
23
|
+
maxBudget: config.getOptionalNumber('litellm.provisioning.defaults.maxBudget') ?? 10,
|
|
24
|
+
budgetDuration: config.getOptionalString('litellm.provisioning.defaults.budgetDuration') ?? '30d',
|
|
25
|
+
models: config.getOptionalStringArray('litellm.provisioning.defaults.models') ?? [],
|
|
26
|
+
teams: config.getOptionalStringArray('litellm.provisioning.defaults.teams') ?? [],
|
|
27
|
+
tpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.tpmLimit'),
|
|
28
|
+
rpmLimit: config.getOptionalNumber('litellm.provisioning.defaults.rpmLimit'),
|
|
29
|
+
metadata: (config.getOptional('litellm.provisioning.defaults.metadata') ?? {}),
|
|
30
|
+
};
|
|
31
|
+
return { enabled, defaults };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extracts the authenticated Backstage user identity from the request token.
|
|
35
|
+
* Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
|
|
36
|
+
* the request carries no user credential (service-to-service calls).
|
|
37
|
+
*/
|
|
38
|
+
async function resolveUserId(req, auth) {
|
|
39
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
40
|
+
if (!rawToken)
|
|
41
|
+
return undefined;
|
|
42
|
+
try {
|
|
43
|
+
const credentials = await auth.authenticate(rawToken);
|
|
44
|
+
const principal = credentials.principal;
|
|
45
|
+
if (principal?.type === 'user') {
|
|
46
|
+
return principal.userEntityRef;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// invalid or service token — caller gets query-param fallback
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Creates a LiteLLM user for the given Backstage identity using the configured
|
|
56
|
+
* defaults. Returns the UserInfo of the newly created account.
|
|
57
|
+
*/
|
|
58
|
+
async function provisionUser(client, userId, defaults, logger) {
|
|
59
|
+
const payload = {
|
|
60
|
+
user_id: userId,
|
|
61
|
+
max_budget: defaults.maxBudget,
|
|
62
|
+
budget_duration: defaults.budgetDuration,
|
|
63
|
+
models: defaults.models,
|
|
64
|
+
teams: defaults.teams,
|
|
65
|
+
...(defaults.tpmLimit !== undefined && { tpm_limit: defaults.tpmLimit }),
|
|
66
|
+
...(defaults.rpmLimit !== undefined && { rpm_limit: defaults.rpmLimit }),
|
|
67
|
+
metadata: {
|
|
68
|
+
...defaults.metadata,
|
|
69
|
+
provisioned_by: 'backstage',
|
|
70
|
+
provisioned_at: new Date().toISOString(),
|
|
71
|
+
backstage_entity: userId,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
logger.info(`Provisioning new LiteLLM user for Backstage identity: ${userId}`);
|
|
75
|
+
try {
|
|
76
|
+
await client.createUser(payload);
|
|
77
|
+
// Fetch the freshly-created user record to return consistent UserInfo shape
|
|
78
|
+
return await client.getUserInfo(userId);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
logger.error(`Failed to provision LiteLLM user ${userId}: ${err.message}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
6
85
|
async function createRouter(options) {
|
|
7
|
-
const { config, logger } = options;
|
|
86
|
+
const { config, logger, auth } = options;
|
|
8
87
|
const baseUrl = config.getString('litellm.baseUrl');
|
|
9
88
|
const masterKey = config.getString('litellm.masterKey');
|
|
10
89
|
const client = new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
90
|
+
const { enabled: provisioningEnabled, defaults: provisioningDefaults } = readProvisioningDefaults(config);
|
|
91
|
+
if (provisioningEnabled) {
|
|
92
|
+
logger.info(`LiteLLM auto-provisioning enabled — defaults: budget=$${provisioningDefaults.maxBudget}/${provisioningDefaults.budgetDuration}, models=${provisioningDefaults.models.length ? provisioningDefaults.models.join(',') : 'all'}, teams=[${provisioningDefaults.teams.join(',')}]`);
|
|
93
|
+
}
|
|
11
94
|
const router = (0, express_1.Router)();
|
|
12
95
|
router.get('/health', (_req, res) => {
|
|
13
|
-
res.json({ status: 'ok' });
|
|
96
|
+
res.json({ status: 'ok', provisioning: provisioningEnabled });
|
|
14
97
|
});
|
|
15
98
|
router.get('/user/info', async (req, res) => {
|
|
16
99
|
try {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
100
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
101
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
102
|
+
let userInfo = await client.getUserInfo(userId);
|
|
103
|
+
if (!userInfo) {
|
|
104
|
+
if (provisioningEnabled && userId) {
|
|
105
|
+
userInfo = await provisionUser(client, userId, provisioningDefaults, logger);
|
|
106
|
+
}
|
|
107
|
+
if (!userInfo) {
|
|
108
|
+
res.status(404).json({
|
|
109
|
+
error: 'User not found in LiteLLM',
|
|
110
|
+
provisioning: provisioningEnabled,
|
|
111
|
+
hint: provisioningEnabled
|
|
112
|
+
? 'Provisioning attempted but failed — check LiteLLM logs'
|
|
113
|
+
: 'Enable litellm.provisioning.enabled in app-config.yaml or create the user manually',
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
19
118
|
res.json(userInfo);
|
|
20
119
|
}
|
|
21
120
|
catch (error) {
|
|
@@ -25,7 +124,8 @@ async function createRouter(options) {
|
|
|
25
124
|
});
|
|
26
125
|
router.get('/keys', async (req, res) => {
|
|
27
126
|
try {
|
|
28
|
-
const
|
|
127
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
128
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
29
129
|
const keys = await client.listKeys(userId);
|
|
30
130
|
res.json(keys);
|
|
31
131
|
}
|
|
@@ -36,7 +136,11 @@ async function createRouter(options) {
|
|
|
36
136
|
});
|
|
37
137
|
router.post('/keys/generate', async (req, res) => {
|
|
38
138
|
try {
|
|
39
|
-
const
|
|
139
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
140
|
+
const request = {
|
|
141
|
+
...req.body,
|
|
142
|
+
...(tokenUserId && { user_id: tokenUserId }),
|
|
143
|
+
};
|
|
40
144
|
const result = await client.generateKey(request);
|
|
41
145
|
res.json(result);
|
|
42
146
|
}
|
|
@@ -70,14 +174,52 @@ async function createRouter(options) {
|
|
|
70
174
|
res.status(500).json({ error: error.message });
|
|
71
175
|
}
|
|
72
176
|
});
|
|
177
|
+
router.get('/teams', async (req, res) => {
|
|
178
|
+
try {
|
|
179
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
180
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
181
|
+
const userInfo = await client.getUserInfo(userId);
|
|
182
|
+
if (!userInfo?.teams?.length) {
|
|
183
|
+
res.json([]);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const teams = await Promise.all(userInfo.teams.map(teamId => client.getTeamInfo(teamId).catch(err => {
|
|
187
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
188
|
+
return null;
|
|
189
|
+
})));
|
|
190
|
+
res.json(teams.filter(Boolean));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger.error('Failed to fetch teams', error);
|
|
194
|
+
res.status(500).json({ error: error.message });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
router.get('/teams/:teamId/usage', async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const { teamId } = req.params;
|
|
200
|
+
const { start_date, end_date } = req.query;
|
|
201
|
+
if (!start_date || !end_date) {
|
|
202
|
+
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const usage = await client.getTeamUsage(teamId, start_date, end_date);
|
|
206
|
+
res.json(usage);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
logger.error('Failed to fetch team usage', error);
|
|
210
|
+
res.status(500).json({ error: error.message });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
73
213
|
router.get('/usage', async (req, res) => {
|
|
74
214
|
try {
|
|
75
|
-
const { start_date, end_date,
|
|
215
|
+
const { start_date, end_date, group_by } = req.query;
|
|
76
216
|
if (!start_date || !end_date) {
|
|
77
217
|
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
78
218
|
return;
|
|
79
219
|
}
|
|
80
|
-
const
|
|
220
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
221
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
222
|
+
const usage = await client.getUsage(start_date, end_date, userId, group_by);
|
|
81
223
|
res.json(usage);
|
|
82
224
|
}
|
|
83
225
|
catch (error) {
|
package/dist/types.cjs.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/types.ts"],
|
|
4
|
-
"sourcesContent": ["export interface UserInfo {\n user_id: string;\n email
|
|
4
|
+
"sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n usage_by_model: Record<string, {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n }>;\n daily_usage: Array<{\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n }>;\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n\nexport interface ProvisioningDefaults {\n maxBudget: number;\n budgetDuration: string;\n models: string[];\n teams: string[];\n tpmLimit?: number;\n rpmLimit?: number;\n metadata: Record<string, string>;\n}\n\nexport interface CreateUserRequest {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n budget_duration?: string;\n models?: string[];\n teams?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n metadata?: Record<string, string>;\n}\n\nexport interface CreateUserResponse {\n user_id: string;\n user_email?: string;\n max_budget?: number;\n models?: string[];\n teams?: string[];\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
export interface UserInfo {
|
|
2
2
|
user_id: string;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
user_email?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
teams?: string[];
|
|
6
|
+
models?: string[];
|
|
6
7
|
max_budget?: number;
|
|
8
|
+
spend?: number;
|
|
7
9
|
current_spend?: number;
|
|
8
10
|
soft_limit?: number;
|
|
9
11
|
hard_limit?: number;
|
|
10
12
|
}
|
|
13
|
+
export interface TeamMember {
|
|
14
|
+
user_id: string;
|
|
15
|
+
role: 'admin' | 'user';
|
|
16
|
+
}
|
|
17
|
+
export interface TeamInfo {
|
|
18
|
+
team_id: string;
|
|
19
|
+
team_alias?: string;
|
|
20
|
+
max_budget?: number;
|
|
21
|
+
spend: number;
|
|
22
|
+
members_with_roles?: TeamMember[];
|
|
23
|
+
models?: string[];
|
|
24
|
+
tpm_limit?: number;
|
|
25
|
+
rpm_limit?: number;
|
|
26
|
+
}
|
|
11
27
|
export interface VirtualKey {
|
|
12
28
|
key: string;
|
|
13
29
|
key_alias?: string;
|
|
@@ -72,3 +88,30 @@ export interface LiteLLMConfig {
|
|
|
72
88
|
baseUrl: string;
|
|
73
89
|
masterKey: string;
|
|
74
90
|
}
|
|
91
|
+
export interface ProvisioningDefaults {
|
|
92
|
+
maxBudget: number;
|
|
93
|
+
budgetDuration: string;
|
|
94
|
+
models: string[];
|
|
95
|
+
teams: string[];
|
|
96
|
+
tpmLimit?: number;
|
|
97
|
+
rpmLimit?: number;
|
|
98
|
+
metadata: Record<string, string>;
|
|
99
|
+
}
|
|
100
|
+
export interface CreateUserRequest {
|
|
101
|
+
user_id: string;
|
|
102
|
+
user_email?: string;
|
|
103
|
+
max_budget?: number;
|
|
104
|
+
budget_duration?: string;
|
|
105
|
+
models?: string[];
|
|
106
|
+
teams?: string[];
|
|
107
|
+
tpm_limit?: number;
|
|
108
|
+
rpm_limit?: number;
|
|
109
|
+
metadata?: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
export interface CreateUserResponse {
|
|
112
|
+
user_id: string;
|
|
113
|
+
user_email?: string;
|
|
114
|
+
max_budget?: number;
|
|
115
|
+
models?: string[];
|
|
116
|
+
teams?: string[];
|
|
117
|
+
}
|