@acarmisc/backstage-plugin-litellm-backend 0.2.0 → 0.2.1
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/dist/client.d.ts +31 -0
- package/dist/client.js +67 -7
- package/dist/index.cjs.js +119 -12
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/provisioning.d.ts +17 -2
- package/dist/provisioning.js +168 -25
- package/dist/router.js +82 -4
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/package.json +3 -3
- package/dist/index.esm.js +0 -323
- package/dist/index.esm.js.map +0 -7
package/dist/client.d.ts
CHANGED
|
@@ -11,7 +11,38 @@ export declare class LiteLLMClient {
|
|
|
11
11
|
*/
|
|
12
12
|
getUserInfo(userId?: string): Promise<UserInfo | null>;
|
|
13
13
|
createUser(payload: CreateUserRequest): Promise<CreateUserResponse>;
|
|
14
|
+
/**
|
|
15
|
+
* Updates an existing LiteLLM user record. Used as a defensive follow-up
|
|
16
|
+
* after /user/new because the upsert path of /user/new has been observed
|
|
17
|
+
* to silently drop fields like user_role under concurrent inserts.
|
|
18
|
+
*/
|
|
19
|
+
updateUser(payload: Partial<CreateUserRequest> & {
|
|
20
|
+
user_id: string;
|
|
21
|
+
}): Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the keys belonging to a user.
|
|
24
|
+
*
|
|
25
|
+
* Implementation note: LiteLLM's `/key/info` endpoint requires a `key`
|
|
26
|
+
* hash and returns 404 when only `user_id` is passed. The correct way
|
|
27
|
+
* to enumerate a user's keys is `/user/info?user_id=X`, which embeds
|
|
28
|
+
* a `keys` array with per-key metadata. We unwrap that array and
|
|
29
|
+
* normalise field names to match the frontend VirtualKey shape
|
|
30
|
+
* (LiteLLM exposes `key_name` for the masked display value and
|
|
31
|
+
* `expires` instead of `expires_at`).
|
|
32
|
+
*/
|
|
14
33
|
listKeys(userId?: string): Promise<VirtualKey[]>;
|
|
34
|
+
private toVirtualKey;
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new virtual key on the LiteLLM proxy.
|
|
37
|
+
*
|
|
38
|
+
* Implementation notes — both required to avoid silently-empty keys:
|
|
39
|
+
* 1. The body must be the plain payload. An earlier version wrapped
|
|
40
|
+
* it as `{ json: request }`; LiteLLM doesn't unwrap that envelope
|
|
41
|
+
* and treats the request as having no fields, returning a key
|
|
42
|
+
* with null alias / models / budget / limits.
|
|
43
|
+
* 2. LiteLLM expects `key_alias`, not `alias`. Without the rename,
|
|
44
|
+
* the alias the user typed is dropped on the floor.
|
|
45
|
+
*/
|
|
15
46
|
generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse>;
|
|
16
47
|
updateKey(request: UpdateKeyRequest): Promise<VirtualKey>;
|
|
17
48
|
deleteKeys(request: DeleteKeyRequest): Promise<{
|
package/dist/client.js
CHANGED
|
@@ -17,7 +17,7 @@ class LiteLLMClient {
|
|
|
17
17
|
signal: controller.signal,
|
|
18
18
|
headers: {
|
|
19
19
|
'Content-Type': 'application/json',
|
|
20
|
-
|
|
20
|
+
Authorization: `Bearer ${this.masterKey}`,
|
|
21
21
|
...options.headers,
|
|
22
22
|
},
|
|
23
23
|
});
|
|
@@ -54,11 +54,35 @@ class LiteLLMClient {
|
|
|
54
54
|
body: JSON.stringify(payload),
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Updates an existing LiteLLM user record. Used as a defensive follow-up
|
|
59
|
+
* after /user/new because the upsert path of /user/new has been observed
|
|
60
|
+
* to silently drop fields like user_role under concurrent inserts.
|
|
61
|
+
*/
|
|
62
|
+
async updateUser(payload) {
|
|
63
|
+
return this.request('/user/update', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
body: JSON.stringify(payload),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the keys belonging to a user.
|
|
70
|
+
*
|
|
71
|
+
* Implementation note: LiteLLM's `/key/info` endpoint requires a `key`
|
|
72
|
+
* hash and returns 404 when only `user_id` is passed. The correct way
|
|
73
|
+
* to enumerate a user's keys is `/user/info?user_id=X`, which embeds
|
|
74
|
+
* a `keys` array with per-key metadata. We unwrap that array and
|
|
75
|
+
* normalise field names to match the frontend VirtualKey shape
|
|
76
|
+
* (LiteLLM exposes `key_name` for the masked display value and
|
|
77
|
+
* `expires` instead of `expires_at`).
|
|
78
|
+
*/
|
|
57
79
|
async listKeys(userId) {
|
|
58
|
-
|
|
80
|
+
if (!userId)
|
|
81
|
+
return [];
|
|
59
82
|
try {
|
|
60
|
-
const response = await this.request(`/
|
|
61
|
-
|
|
83
|
+
const response = await this.request(`/user/info?user_id=${encodeURIComponent(userId)}`);
|
|
84
|
+
const rawKeys = response.keys ?? [];
|
|
85
|
+
return rawKeys.map(this.toVirtualKey);
|
|
62
86
|
}
|
|
63
87
|
catch (err) {
|
|
64
88
|
if (err.status === 404 || err.message.includes('not found')) {
|
|
@@ -67,10 +91,44 @@ class LiteLLMClient {
|
|
|
67
91
|
throw err;
|
|
68
92
|
}
|
|
69
93
|
}
|
|
94
|
+
toVirtualKey(k) {
|
|
95
|
+
return {
|
|
96
|
+
// The hashed `token` never leaves LiteLLM in a usable form; the
|
|
97
|
+
// masked `key_name` ("sk-...XXXX") is what the UI displays. Fall
|
|
98
|
+
// back to `token` only when `key_name` is missing.
|
|
99
|
+
key: k.key_name ?? k.token,
|
|
100
|
+
token: k.token,
|
|
101
|
+
key_alias: k.key_alias ?? undefined,
|
|
102
|
+
created_at: k.created_at,
|
|
103
|
+
expires_at: k.expires ?? undefined,
|
|
104
|
+
spend: k.spend ?? 0,
|
|
105
|
+
max_budget: k.max_budget ?? undefined,
|
|
106
|
+
tpm_limit: k.tpm_limit ?? undefined,
|
|
107
|
+
rpm_limit: k.rpm_limit ?? undefined,
|
|
108
|
+
models: k.models ?? [],
|
|
109
|
+
user_id: k.user_id ?? undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Creates a new virtual key on the LiteLLM proxy.
|
|
114
|
+
*
|
|
115
|
+
* Implementation notes — both required to avoid silently-empty keys:
|
|
116
|
+
* 1. The body must be the plain payload. An earlier version wrapped
|
|
117
|
+
* it as `{ json: request }`; LiteLLM doesn't unwrap that envelope
|
|
118
|
+
* and treats the request as having no fields, returning a key
|
|
119
|
+
* with null alias / models / budget / limits.
|
|
120
|
+
* 2. LiteLLM expects `key_alias`, not `alias`. Without the rename,
|
|
121
|
+
* the alias the user typed is dropped on the floor.
|
|
122
|
+
*/
|
|
70
123
|
async generateKey(request) {
|
|
124
|
+
const { alias, ...rest } = request;
|
|
125
|
+
const payload = {
|
|
126
|
+
...rest,
|
|
127
|
+
...(alias && { key_alias: alias }),
|
|
128
|
+
};
|
|
71
129
|
return this.request('/key/generate', {
|
|
72
130
|
method: 'POST',
|
|
73
|
-
body: JSON.stringify(
|
|
131
|
+
body: JSON.stringify(payload),
|
|
74
132
|
});
|
|
75
133
|
}
|
|
76
134
|
async updateKey(request) {
|
|
@@ -87,7 +145,7 @@ class LiteLLMClient {
|
|
|
87
145
|
}
|
|
88
146
|
async listModels() {
|
|
89
147
|
const response = await this.request('/models');
|
|
90
|
-
return Array.isArray(response) ? response :
|
|
148
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
91
149
|
}
|
|
92
150
|
async getTeamInfo(teamId) {
|
|
93
151
|
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
@@ -120,7 +178,9 @@ class LiteLLMClient {
|
|
|
120
178
|
* - usage_by_key → which keys drove cost / traffic (with key_alias + team_id from metadata)
|
|
121
179
|
*/
|
|
122
180
|
transformDailyActivity(response) {
|
|
123
|
-
const results = Array.isArray(response?.results)
|
|
181
|
+
const results = Array.isArray(response?.results)
|
|
182
|
+
? response.results
|
|
183
|
+
: [];
|
|
124
184
|
const meta = response?.metadata ?? {};
|
|
125
185
|
const daily_usage = results
|
|
126
186
|
.map(r => ({
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,6 +32,7 @@ var index_exports = {};
|
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
LiteLLMClient: () => LiteLLMClient,
|
|
24
34
|
createRouter: () => createRouter,
|
|
35
|
+
default: () => litellmPlugin,
|
|
25
36
|
litellmPlugin: () => litellmPlugin
|
|
26
37
|
});
|
|
27
38
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -30,7 +41,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
41
|
var import_backend_plugin_api = require("@backstage/backend-plugin-api");
|
|
31
42
|
|
|
32
43
|
// src/router.ts
|
|
33
|
-
var import_express = require("express");
|
|
44
|
+
var import_express = __toESM(require("express"));
|
|
34
45
|
var import_catalog_client = require("@backstage/catalog-client");
|
|
35
46
|
|
|
36
47
|
// src/client.ts
|
|
@@ -50,13 +61,15 @@ var LiteLLMClient = class {
|
|
|
50
61
|
signal: controller.signal,
|
|
51
62
|
headers: {
|
|
52
63
|
"Content-Type": "application/json",
|
|
53
|
-
|
|
64
|
+
Authorization: `Bearer ${this.masterKey}`,
|
|
54
65
|
...options.headers
|
|
55
66
|
}
|
|
56
67
|
});
|
|
57
68
|
if (!response.ok) {
|
|
58
69
|
const errorBody = await response.text();
|
|
59
|
-
const err = new Error(
|
|
70
|
+
const err = new Error(
|
|
71
|
+
`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`
|
|
72
|
+
);
|
|
60
73
|
err.status = response.status;
|
|
61
74
|
throw err;
|
|
62
75
|
}
|
|
@@ -84,11 +97,36 @@ var LiteLLMClient = class {
|
|
|
84
97
|
body: JSON.stringify(payload)
|
|
85
98
|
});
|
|
86
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Updates an existing LiteLLM user record. Used as a defensive follow-up
|
|
102
|
+
* after /user/new because the upsert path of /user/new has been observed
|
|
103
|
+
* to silently drop fields like user_role under concurrent inserts.
|
|
104
|
+
*/
|
|
105
|
+
async updateUser(payload) {
|
|
106
|
+
return this.request("/user/update", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
body: JSON.stringify(payload)
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns the keys belonging to a user.
|
|
113
|
+
*
|
|
114
|
+
* Implementation note: LiteLLM's `/key/info` endpoint requires a `key`
|
|
115
|
+
* hash and returns 404 when only `user_id` is passed. The correct way
|
|
116
|
+
* to enumerate a user's keys is `/user/info?user_id=X`, which embeds
|
|
117
|
+
* a `keys` array with per-key metadata. We unwrap that array and
|
|
118
|
+
* normalise field names to match the frontend VirtualKey shape
|
|
119
|
+
* (LiteLLM exposes `key_name` for the masked display value and
|
|
120
|
+
* `expires` instead of `expires_at`).
|
|
121
|
+
*/
|
|
87
122
|
async listKeys(userId) {
|
|
88
|
-
|
|
123
|
+
if (!userId) return [];
|
|
89
124
|
try {
|
|
90
|
-
const response = await this.request(
|
|
91
|
-
|
|
125
|
+
const response = await this.request(
|
|
126
|
+
`/user/info?user_id=${encodeURIComponent(userId)}`
|
|
127
|
+
);
|
|
128
|
+
const rawKeys = response.keys ?? [];
|
|
129
|
+
return rawKeys.map(this.toVirtualKey);
|
|
92
130
|
} catch (err) {
|
|
93
131
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
94
132
|
return [];
|
|
@@ -96,10 +134,44 @@ var LiteLLMClient = class {
|
|
|
96
134
|
throw err;
|
|
97
135
|
}
|
|
98
136
|
}
|
|
137
|
+
toVirtualKey(k) {
|
|
138
|
+
return {
|
|
139
|
+
// The hashed `token` never leaves LiteLLM in a usable form; the
|
|
140
|
+
// masked `key_name` ("sk-...XXXX") is what the UI displays. Fall
|
|
141
|
+
// back to `token` only when `key_name` is missing.
|
|
142
|
+
key: k.key_name ?? k.token,
|
|
143
|
+
token: k.token,
|
|
144
|
+
key_alias: k.key_alias ?? void 0,
|
|
145
|
+
created_at: k.created_at,
|
|
146
|
+
expires_at: k.expires ?? void 0,
|
|
147
|
+
spend: k.spend ?? 0,
|
|
148
|
+
max_budget: k.max_budget ?? void 0,
|
|
149
|
+
tpm_limit: k.tpm_limit ?? void 0,
|
|
150
|
+
rpm_limit: k.rpm_limit ?? void 0,
|
|
151
|
+
models: k.models ?? [],
|
|
152
|
+
user_id: k.user_id ?? void 0
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Creates a new virtual key on the LiteLLM proxy.
|
|
157
|
+
*
|
|
158
|
+
* Implementation notes — both required to avoid silently-empty keys:
|
|
159
|
+
* 1. The body must be the plain payload. An earlier version wrapped
|
|
160
|
+
* it as `{ json: request }`; LiteLLM doesn't unwrap that envelope
|
|
161
|
+
* and treats the request as having no fields, returning a key
|
|
162
|
+
* with null alias / models / budget / limits.
|
|
163
|
+
* 2. LiteLLM expects `key_alias`, not `alias`. Without the rename,
|
|
164
|
+
* the alias the user typed is dropped on the floor.
|
|
165
|
+
*/
|
|
99
166
|
async generateKey(request) {
|
|
167
|
+
const { alias, ...rest } = request;
|
|
168
|
+
const payload = {
|
|
169
|
+
...rest,
|
|
170
|
+
...alias && { key_alias: alias }
|
|
171
|
+
};
|
|
100
172
|
return this.request("/key/generate", {
|
|
101
173
|
method: "POST",
|
|
102
|
-
body: JSON.stringify(
|
|
174
|
+
body: JSON.stringify(payload)
|
|
103
175
|
});
|
|
104
176
|
}
|
|
105
177
|
async updateKey(request) {
|
|
@@ -115,11 +187,15 @@ var LiteLLMClient = class {
|
|
|
115
187
|
});
|
|
116
188
|
}
|
|
117
189
|
async listModels() {
|
|
118
|
-
const response = await this.request(
|
|
190
|
+
const response = await this.request(
|
|
191
|
+
"/models"
|
|
192
|
+
);
|
|
119
193
|
return Array.isArray(response) ? response : response.data ?? [];
|
|
120
194
|
}
|
|
121
195
|
async getTeamInfo(teamId) {
|
|
122
|
-
return this.request(
|
|
196
|
+
return this.request(
|
|
197
|
+
`/team/info?team_id=${encodeURIComponent(teamId)}`
|
|
198
|
+
);
|
|
123
199
|
}
|
|
124
200
|
emptyUsage() {
|
|
125
201
|
return {
|
|
@@ -243,7 +319,9 @@ var LiteLLMClient = class {
|
|
|
243
319
|
});
|
|
244
320
|
if (userId) params.append("user_id", userId);
|
|
245
321
|
try {
|
|
246
|
-
const response = await this.request(
|
|
322
|
+
const response = await this.request(
|
|
323
|
+
`/user/daily/activity?${params.toString()}`
|
|
324
|
+
);
|
|
247
325
|
return this.transformDailyActivity(response);
|
|
248
326
|
} catch (err) {
|
|
249
327
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
@@ -260,7 +338,9 @@ var LiteLLMClient = class {
|
|
|
260
338
|
page_size: "100"
|
|
261
339
|
});
|
|
262
340
|
try {
|
|
263
|
-
const response = await this.request(
|
|
341
|
+
const response = await this.request(
|
|
342
|
+
`/team/daily/activity?${params.toString()}`
|
|
343
|
+
);
|
|
264
344
|
return this.transformDailyActivity(response);
|
|
265
345
|
} catch (err) {
|
|
266
346
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
@@ -370,6 +450,7 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
|
|
|
370
450
|
...defaults.tpmLimit !== void 0 && { tpm_limit: defaults.tpmLimit },
|
|
371
451
|
...defaults.rpmLimit !== void 0 && { rpm_limit: defaults.rpmLimit },
|
|
372
452
|
...defaults.userRole && { user_role: defaults.userRole },
|
|
453
|
+
auto_create_key: false,
|
|
373
454
|
metadata: {
|
|
374
455
|
...defaults.metadata,
|
|
375
456
|
provisioned_by: "backstage",
|
|
@@ -537,6 +618,7 @@ async function createRouter(options) {
|
|
|
537
618
|
);
|
|
538
619
|
}
|
|
539
620
|
const router = (0, import_express.Router)();
|
|
621
|
+
router.use(import_express.default.json());
|
|
540
622
|
router.get("/health", (_req, res) => {
|
|
541
623
|
res.json({ status: "ok", provisioning: provisioningEnabled });
|
|
542
624
|
});
|
|
@@ -593,6 +675,19 @@ async function createRouter(options) {
|
|
|
593
675
|
});
|
|
594
676
|
router.post("/keys/generate", async (req, res) => {
|
|
595
677
|
try {
|
|
678
|
+
const body = req.body ?? {};
|
|
679
|
+
const missing = [];
|
|
680
|
+
if (!body.alias?.trim()) missing.push("alias");
|
|
681
|
+
if (typeof body.max_budget !== "number" || body.max_budget <= 0) {
|
|
682
|
+
missing.push("max_budget (positive number)");
|
|
683
|
+
}
|
|
684
|
+
if (missing.length) {
|
|
685
|
+
res.status(400).json({
|
|
686
|
+
error: "Missing required fields",
|
|
687
|
+
hint: `Required: ${missing.join(", ")}`
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
596
691
|
const tokenEntityRef = await resolveUserId(req, auth);
|
|
597
692
|
const resolvedUserId = tokenEntityRef ? toLiteLLMUserId(tokenEntityRef, userIdDomain) : void 0;
|
|
598
693
|
if (resolvedUserId) {
|
|
@@ -608,8 +703,20 @@ async function createRouter(options) {
|
|
|
608
703
|
logger
|
|
609
704
|
);
|
|
610
705
|
}
|
|
706
|
+
const profile = tokenEntityRef ? await resolveUserProfile(tokenEntityRef, catalogClient, auth, logger) : {};
|
|
707
|
+
const enrichedMetadata = {
|
|
708
|
+
...body.metadata ?? {},
|
|
709
|
+
created_by_backstage_user: tokenEntityRef ?? "unknown",
|
|
710
|
+
...profile.email && { created_by_email: profile.email },
|
|
711
|
+
...profile.displayName && {
|
|
712
|
+
created_by_display_name: profile.displayName
|
|
713
|
+
},
|
|
714
|
+
created_via: "backstage",
|
|
715
|
+
created_at_iso: (/* @__PURE__ */ new Date()).toISOString()
|
|
716
|
+
};
|
|
611
717
|
const request = {
|
|
612
|
-
...
|
|
718
|
+
...body,
|
|
719
|
+
metadata: enrichedMetadata,
|
|
613
720
|
...resolvedUserId && { user_id: resolvedUserId }
|
|
614
721
|
};
|
|
615
722
|
const result = await client.generateKey(request);
|