@acarmisc/backstage-plugin-litellm-backend 0.1.16 → 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 +69 -0
- package/dist/client.js +312 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +26 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +23 -0
- package/dist/provisioning.d.ts +87 -0
- package/dist/provisioning.js +339 -0
- package/dist/router.d.ts +12 -0
- package/dist/router.js +274 -0
- package/dist/types.d.ts +208 -0
- package/dist/types.js +2 -0
- package/package.json +9 -9
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, UpdateKeyRequest, DeleteKeyRequest, CreateUserRequest, CreateUserResponse } from './types';
|
|
2
|
+
export declare class LiteLLMClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private masterKey;
|
|
5
|
+
private timeout;
|
|
6
|
+
constructor(config: LiteLLMConfig, timeout?: number);
|
|
7
|
+
private request;
|
|
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>;
|
|
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
|
+
*/
|
|
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
|
+
*/
|
|
46
|
+
generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse>;
|
|
47
|
+
updateKey(request: UpdateKeyRequest): Promise<VirtualKey>;
|
|
48
|
+
deleteKeys(request: DeleteKeyRequest): Promise<{
|
|
49
|
+
success: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
listModels(): Promise<ModelInfo[]>;
|
|
52
|
+
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
53
|
+
private emptyUsage;
|
|
54
|
+
/**
|
|
55
|
+
* Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
|
|
56
|
+
* UsageMetrics shape consumed by the frontend charts.
|
|
57
|
+
*
|
|
58
|
+
* Source shape (per result row):
|
|
59
|
+
* { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }
|
|
60
|
+
*
|
|
61
|
+
* We fan that out into three views the UI consumes:
|
|
62
|
+
* - daily_usage → spend + request trends over time
|
|
63
|
+
* - usage_by_model → which models drove cost / traffic
|
|
64
|
+
* - usage_by_key → which keys drove cost / traffic (with key_alias + team_id from metadata)
|
|
65
|
+
*/
|
|
66
|
+
private transformDailyActivity;
|
|
67
|
+
getUsage(startDate: string, endDate: string, userId?: string, _groupBy?: string): Promise<UsageMetrics>;
|
|
68
|
+
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
69
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LiteLLMClient = void 0;
|
|
4
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
5
|
+
class LiteLLMClient {
|
|
6
|
+
constructor(config, timeout = DEFAULT_TIMEOUT) {
|
|
7
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
8
|
+
this.masterKey = config.masterKey;
|
|
9
|
+
this.timeout = timeout;
|
|
10
|
+
}
|
|
11
|
+
async request(path, options = {}) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
16
|
+
...options,
|
|
17
|
+
signal: controller.signal,
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
Authorization: `Bearer ${this.masterKey}`,
|
|
21
|
+
...options.headers,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const errorBody = await response.text();
|
|
26
|
+
const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
27
|
+
err.status = response.status;
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
}
|
|
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
|
+
*/
|
|
40
|
+
async getUserInfo(userId) {
|
|
41
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
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
|
+
});
|
|
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
|
+
*/
|
|
79
|
+
async listKeys(userId) {
|
|
80
|
+
if (!userId)
|
|
81
|
+
return [];
|
|
82
|
+
try {
|
|
83
|
+
const response = await this.request(`/user/info?user_id=${encodeURIComponent(userId)}`);
|
|
84
|
+
const rawKeys = response.keys ?? [];
|
|
85
|
+
return rawKeys.map(this.toVirtualKey);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
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
|
+
*/
|
|
123
|
+
async generateKey(request) {
|
|
124
|
+
const { alias, ...rest } = request;
|
|
125
|
+
const payload = {
|
|
126
|
+
...rest,
|
|
127
|
+
...(alias && { key_alias: alias }),
|
|
128
|
+
};
|
|
129
|
+
return this.request('/key/generate', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
body: JSON.stringify(payload),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async updateKey(request) {
|
|
135
|
+
return this.request('/key/update', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: JSON.stringify(request),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async deleteKeys(request) {
|
|
141
|
+
return this.request('/key/delete', {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
body: JSON.stringify(request),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async listModels() {
|
|
147
|
+
const response = await this.request('/models');
|
|
148
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
149
|
+
}
|
|
150
|
+
async getTeamInfo(teamId) {
|
|
151
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
152
|
+
}
|
|
153
|
+
emptyUsage() {
|
|
154
|
+
return {
|
|
155
|
+
total_spend: 0,
|
|
156
|
+
total_tokens: 0,
|
|
157
|
+
prompt_tokens: 0,
|
|
158
|
+
completion_tokens: 0,
|
|
159
|
+
api_requests: 0,
|
|
160
|
+
successful_requests: 0,
|
|
161
|
+
failed_requests: 0,
|
|
162
|
+
usage_by_model: {},
|
|
163
|
+
usage_by_key: {},
|
|
164
|
+
daily_usage: [],
|
|
165
|
+
daily_by_model: [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
|
|
170
|
+
* UsageMetrics shape consumed by the frontend charts.
|
|
171
|
+
*
|
|
172
|
+
* Source shape (per result row):
|
|
173
|
+
* { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }
|
|
174
|
+
*
|
|
175
|
+
* We fan that out into three views the UI consumes:
|
|
176
|
+
* - daily_usage → spend + request trends over time
|
|
177
|
+
* - usage_by_model → which models drove cost / traffic
|
|
178
|
+
* - usage_by_key → which keys drove cost / traffic (with key_alias + team_id from metadata)
|
|
179
|
+
*/
|
|
180
|
+
transformDailyActivity(response) {
|
|
181
|
+
const results = Array.isArray(response?.results)
|
|
182
|
+
? response.results
|
|
183
|
+
: [];
|
|
184
|
+
const meta = response?.metadata ?? {};
|
|
185
|
+
const daily_usage = results
|
|
186
|
+
.map(r => ({
|
|
187
|
+
date: r.date,
|
|
188
|
+
spend: r.metrics?.spend ?? 0,
|
|
189
|
+
total_tokens: r.metrics?.total_tokens ?? 0,
|
|
190
|
+
prompt_tokens: r.metrics?.prompt_tokens ?? 0,
|
|
191
|
+
completion_tokens: r.metrics?.completion_tokens ?? 0,
|
|
192
|
+
api_requests: r.metrics?.api_requests ?? 0,
|
|
193
|
+
successful_requests: r.metrics?.successful_requests ?? 0,
|
|
194
|
+
failed_requests: r.metrics?.failed_requests ?? 0,
|
|
195
|
+
}))
|
|
196
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
197
|
+
const usage_by_model = {};
|
|
198
|
+
const usage_by_key = {};
|
|
199
|
+
const daily_by_model = [];
|
|
200
|
+
const emptyModelBucket = () => ({
|
|
201
|
+
total_spend: 0,
|
|
202
|
+
total_tokens: 0,
|
|
203
|
+
prompt_tokens: 0,
|
|
204
|
+
completion_tokens: 0,
|
|
205
|
+
api_requests: 0,
|
|
206
|
+
successful_requests: 0,
|
|
207
|
+
failed_requests: 0,
|
|
208
|
+
});
|
|
209
|
+
for (const r of results) {
|
|
210
|
+
const models = r.breakdown?.models ?? {};
|
|
211
|
+
for (const [name, entry] of Object.entries(models)) {
|
|
212
|
+
const m = entry?.metrics ?? {};
|
|
213
|
+
const bucket = usage_by_model[name] ?? emptyModelBucket();
|
|
214
|
+
bucket.total_spend += m.spend ?? 0;
|
|
215
|
+
bucket.total_tokens += m.total_tokens ?? 0;
|
|
216
|
+
bucket.prompt_tokens += m.prompt_tokens ?? 0;
|
|
217
|
+
bucket.completion_tokens += m.completion_tokens ?? 0;
|
|
218
|
+
bucket.api_requests += m.api_requests ?? 0;
|
|
219
|
+
bucket.successful_requests += m.successful_requests ?? 0;
|
|
220
|
+
bucket.failed_requests += m.failed_requests ?? 0;
|
|
221
|
+
usage_by_model[name] = bucket;
|
|
222
|
+
daily_by_model.push({
|
|
223
|
+
date: r.date,
|
|
224
|
+
model: name,
|
|
225
|
+
spend: m.spend ?? 0,
|
|
226
|
+
prompt_tokens: m.prompt_tokens ?? 0,
|
|
227
|
+
completion_tokens: m.completion_tokens ?? 0,
|
|
228
|
+
total_tokens: m.total_tokens ?? 0,
|
|
229
|
+
api_requests: m.api_requests ?? 0,
|
|
230
|
+
successful_requests: m.successful_requests ?? 0,
|
|
231
|
+
failed_requests: m.failed_requests ?? 0,
|
|
232
|
+
});
|
|
233
|
+
const keyMap = entry?.api_key_breakdown ?? {};
|
|
234
|
+
for (const [keyHash, keyEntry] of Object.entries(keyMap)) {
|
|
235
|
+
const km = keyEntry?.metrics ?? {};
|
|
236
|
+
const kmeta = keyEntry?.metadata ?? {};
|
|
237
|
+
const kb = usage_by_key[keyHash] ?? {
|
|
238
|
+
key_alias: kmeta.key_alias,
|
|
239
|
+
team_id: kmeta.team_id ?? null,
|
|
240
|
+
models: [],
|
|
241
|
+
...emptyModelBucket(),
|
|
242
|
+
};
|
|
243
|
+
if (!kb.key_alias && kmeta.key_alias)
|
|
244
|
+
kb.key_alias = kmeta.key_alias;
|
|
245
|
+
if (kb.team_id == null && kmeta.team_id)
|
|
246
|
+
kb.team_id = kmeta.team_id;
|
|
247
|
+
if (!kb.models.includes(name))
|
|
248
|
+
kb.models.push(name);
|
|
249
|
+
kb.total_spend += km.spend ?? 0;
|
|
250
|
+
kb.total_tokens += km.total_tokens ?? 0;
|
|
251
|
+
kb.prompt_tokens += km.prompt_tokens ?? 0;
|
|
252
|
+
kb.completion_tokens += km.completion_tokens ?? 0;
|
|
253
|
+
kb.api_requests += km.api_requests ?? 0;
|
|
254
|
+
kb.successful_requests += km.successful_requests ?? 0;
|
|
255
|
+
kb.failed_requests += km.failed_requests ?? 0;
|
|
256
|
+
usage_by_key[keyHash] = kb;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
total_spend: meta.total_spend ?? 0,
|
|
262
|
+
total_tokens: meta.total_tokens ?? 0,
|
|
263
|
+
prompt_tokens: meta.total_prompt_tokens ?? 0,
|
|
264
|
+
completion_tokens: meta.total_completion_tokens ?? 0,
|
|
265
|
+
api_requests: meta.total_api_requests ?? 0,
|
|
266
|
+
successful_requests: meta.total_successful_requests ?? 0,
|
|
267
|
+
failed_requests: meta.total_failed_requests ?? 0,
|
|
268
|
+
usage_by_model,
|
|
269
|
+
usage_by_key,
|
|
270
|
+
daily_usage,
|
|
271
|
+
daily_by_model,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async getUsage(startDate, endDate, userId, _groupBy) {
|
|
275
|
+
const params = new URLSearchParams({
|
|
276
|
+
start_date: startDate,
|
|
277
|
+
end_date: endDate,
|
|
278
|
+
page_size: '100',
|
|
279
|
+
});
|
|
280
|
+
if (userId)
|
|
281
|
+
params.append('user_id', userId);
|
|
282
|
+
try {
|
|
283
|
+
const response = await this.request(`/user/daily/activity?${params.toString()}`);
|
|
284
|
+
return this.transformDailyActivity(response);
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
288
|
+
return this.emptyUsage();
|
|
289
|
+
}
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
294
|
+
const params = new URLSearchParams({
|
|
295
|
+
start_date: startDate,
|
|
296
|
+
end_date: endDate,
|
|
297
|
+
team_ids: teamId,
|
|
298
|
+
page_size: '100',
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
const response = await this.request(`/team/daily/activity?${params.toString()}`);
|
|
302
|
+
return this.transformDailyActivity(response);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
306
|
+
return this.emptyUsage();
|
|
307
|
+
}
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.LiteLLMClient = exports.createRouter = exports.default = exports.litellmPlugin = void 0;
|
|
18
|
+
var plugin_1 = require("./plugin");
|
|
19
|
+
Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
|
|
20
|
+
var plugin_2 = require("./plugin");
|
|
21
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return plugin_2.litellmPlugin; } });
|
|
22
|
+
var router_1 = require("./router");
|
|
23
|
+
Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } });
|
|
24
|
+
__exportStar(require("./types"), exports);
|
|
25
|
+
var client_1 = require("./client");
|
|
26
|
+
Object.defineProperty(exports, "LiteLLMClient", { enumerable: true, get: function () { return client_1.LiteLLMClient; } });
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const litellmPlugin: import("@backstage/backend-plugin-api").BackendFeature;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.litellmPlugin = void 0;
|
|
4
|
+
const backend_plugin_api_1 = require("@backstage/backend-plugin-api");
|
|
5
|
+
const router_1 = require("./router");
|
|
6
|
+
exports.litellmPlugin = (0, backend_plugin_api_1.createBackendPlugin)({
|
|
7
|
+
pluginId: 'litellm',
|
|
8
|
+
register(reg) {
|
|
9
|
+
reg.registerInit({
|
|
10
|
+
deps: {
|
|
11
|
+
httpRouter: backend_plugin_api_1.coreServices.httpRouter,
|
|
12
|
+
config: backend_plugin_api_1.coreServices.rootConfig,
|
|
13
|
+
logger: backend_plugin_api_1.coreServices.logger,
|
|
14
|
+
auth: backend_plugin_api_1.coreServices.auth,
|
|
15
|
+
discovery: backend_plugin_api_1.coreServices.discovery,
|
|
16
|
+
},
|
|
17
|
+
async init({ httpRouter, config, logger, auth, discovery }) {
|
|
18
|
+
const router = await (0, router_1.createRouter)({ config, logger, auth, discovery });
|
|
19
|
+
httpRouter.use(router);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Config } from '@backstage/config';
|
|
2
|
+
import { AuthService } from '@backstage/backend-plugin-api';
|
|
3
|
+
import { CatalogClient } from '@backstage/catalog-client';
|
|
4
|
+
import { Request } from 'express';
|
|
5
|
+
import { LiteLLMClient } from './client';
|
|
6
|
+
import { UserInfo, ProvisioningDefaults, RoleConfig } from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Converts a Backstage user entity ref to a LiteLLM user_id.
|
|
9
|
+
*
|
|
10
|
+
* When userIdDomain is configured, the entity name is suffixed with the domain
|
|
11
|
+
* so that LiteLLM user_ids match the organisation's email addresses:
|
|
12
|
+
* "user:default/andrea.carmisciano" + "abstract.it"
|
|
13
|
+
* → "andrea.carmisciano@abstract.it"
|
|
14
|
+
*
|
|
15
|
+
* Without a domain the bare entity name is returned unchanged, which works for
|
|
16
|
+
* deployments where LiteLLM users were created with plain usernames.
|
|
17
|
+
*/
|
|
18
|
+
export declare function toLiteLLMUserId(userEntityRef: string, userIdDomain?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Reads the provisioning block from config, applying safe defaults for every
|
|
21
|
+
* field so the feature works out-of-the-box without any YAML required.
|
|
22
|
+
*
|
|
23
|
+
* Safe defaults rationale:
|
|
24
|
+
* maxBudget: $10 — prevents runaway spend on a forgotten test account
|
|
25
|
+
* budgetDuration: 30d — monthly reset, aligns with typical billing cycles
|
|
26
|
+
* models: [] — empty means all proxy models are allowed;
|
|
27
|
+
* restrict here or at team level for tighter control
|
|
28
|
+
* teams: [] — no automatic team assignment; add IDs to enrol users
|
|
29
|
+
* tpmLimit: none — LiteLLM global / team limits still apply
|
|
30
|
+
* rpmLimit: none — same
|
|
31
|
+
* metadata: backstage source tag only
|
|
32
|
+
*/
|
|
33
|
+
export declare function readRoleConfigs(config: Config): RoleConfig[];
|
|
34
|
+
/**
|
|
35
|
+
* Merges role config over defaults. Role fields override defaults only when explicitly set.
|
|
36
|
+
*/
|
|
37
|
+
export declare function applyRoleOverrides(defaults: ProvisioningDefaults, role: RoleConfig): ProvisioningDefaults;
|
|
38
|
+
export declare function readProvisioningDefaults(config: Config): {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
defaults: ProvisioningDefaults;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Extracts the authenticated Backstage user identity from the request token.
|
|
44
|
+
* Returns the userEntityRef (e.g. "user:default/john.doe") or undefined when
|
|
45
|
+
* the request carries no user credential (service-to-service calls).
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveUserId(req: Request, auth: AuthService): Promise<string | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Profile data extracted from a Backstage Catalog User entity, used to
|
|
50
|
+
* populate user_email / user_alias on the LiteLLM record.
|
|
51
|
+
*/
|
|
52
|
+
export interface BackstageUserProfile {
|
|
53
|
+
email?: string;
|
|
54
|
+
displayName?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Looks up the catalog User entity for the authenticated user and returns
|
|
58
|
+
* the profile block. Returns an empty object when the user has no catalog
|
|
59
|
+
* entity (e.g. dangerouslyAllowSignInWithoutUserInCatalog was used) — the
|
|
60
|
+
* caller falls back to deriving identity from userIdDomain.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveUserProfile(userEntityRef: string, catalogClient: CatalogClient, auth: AuthService, logger: any): Promise<BackstageUserProfile>;
|
|
63
|
+
/**
|
|
64
|
+
* Creates a LiteLLM user for the given Backstage identity using the configured
|
|
65
|
+
* defaults. Returns the UserInfo of the newly created account.
|
|
66
|
+
*/
|
|
67
|
+
export declare function provisionUser(client: LiteLLMClient, userId: string, defaults: ProvisioningDefaults, profile: BackstageUserProfile, backstageEntity: string | undefined, logger: any): Promise<UserInfo | null>;
|
|
68
|
+
export declare class ProvisioningError extends Error {
|
|
69
|
+
status: number;
|
|
70
|
+
body: {
|
|
71
|
+
error: string;
|
|
72
|
+
hint: string;
|
|
73
|
+
provisioning: boolean;
|
|
74
|
+
};
|
|
75
|
+
constructor(message: string, hint: string, provisioning: boolean, status?: number);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ensures the LiteLLM user exists, returning its UserInfo.
|
|
79
|
+
* When the user is missing and provisioning is enabled, attempts to create it.
|
|
80
|
+
* When provisioning is disabled, throws a ProvisioningError with a clear message.
|
|
81
|
+
*/
|
|
82
|
+
export declare function getOrProvisionUser(client: LiteLLMClient, tokenEntityRef: string | undefined, userId: string | undefined, provisioningEnabled: boolean, provisioningDefaults: ProvisioningDefaults, roleConfigs: RoleConfig[], catalogClient: CatalogClient, auth: AuthService, logger: any): Promise<UserInfo>;
|
|
83
|
+
/**
|
|
84
|
+
* Fetches the user's Backstage group memberships and returns the first matching
|
|
85
|
+
* role config (priority order), or undefined when no role matches.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveUserRole(userEntityRef: string, roleConfigs: RoleConfig[], catalogClient: CatalogClient, auth: AuthService, logger: any): Promise<RoleConfig | undefined>;
|