@acarmisc/backstage-plugin-litellm-backend 0.1.16 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +38 -0
- package/dist/client.js +252 -0
- package/dist/index.cjs.js +12 -119
- package/dist/index.cjs.js.map +3 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +323 -0
- package/dist/index.esm.js.map +7 -0
- package/dist/index.js +24 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +23 -0
- package/dist/provisioning.d.ts +72 -0
- package/dist/provisioning.js +196 -0
- package/dist/router.d.ts +12 -0
- package/dist/router.js +196 -0
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +175 -0
- package/dist/types.js +2 -0
- package/package.json +7 -7
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
listKeys(userId?: string): Promise<VirtualKey[]>;
|
|
15
|
+
generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse>;
|
|
16
|
+
updateKey(request: UpdateKeyRequest): Promise<VirtualKey>;
|
|
17
|
+
deleteKeys(request: DeleteKeyRequest): Promise<{
|
|
18
|
+
success: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
listModels(): Promise<ModelInfo[]>;
|
|
21
|
+
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
22
|
+
private emptyUsage;
|
|
23
|
+
/**
|
|
24
|
+
* Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
|
|
25
|
+
* UsageMetrics shape consumed by the frontend charts.
|
|
26
|
+
*
|
|
27
|
+
* Source shape (per result row):
|
|
28
|
+
* { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }
|
|
29
|
+
*
|
|
30
|
+
* We fan that out into three views the UI consumes:
|
|
31
|
+
* - daily_usage → spend + request trends over time
|
|
32
|
+
* - usage_by_model → which models drove cost / traffic
|
|
33
|
+
* - usage_by_key → which keys drove cost / traffic (with key_alias + team_id from metadata)
|
|
34
|
+
*/
|
|
35
|
+
private transformDailyActivity;
|
|
36
|
+
getUsage(startDate: string, endDate: string, userId?: string, _groupBy?: string): Promise<UsageMetrics>;
|
|
37
|
+
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
38
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
async listKeys(userId) {
|
|
58
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
59
|
+
try {
|
|
60
|
+
const response = await this.request(`/key/info${query}`);
|
|
61
|
+
return Array.isArray(response) ? response : (response.info ?? []);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async generateKey(request) {
|
|
71
|
+
return this.request('/key/generate', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({ json: request }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async updateKey(request) {
|
|
77
|
+
return this.request('/key/update', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: JSON.stringify(request),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async deleteKeys(request) {
|
|
83
|
+
return this.request('/key/delete', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
body: JSON.stringify(request),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async listModels() {
|
|
89
|
+
const response = await this.request('/models');
|
|
90
|
+
return Array.isArray(response) ? response : (response.data ?? []);
|
|
91
|
+
}
|
|
92
|
+
async getTeamInfo(teamId) {
|
|
93
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
94
|
+
}
|
|
95
|
+
emptyUsage() {
|
|
96
|
+
return {
|
|
97
|
+
total_spend: 0,
|
|
98
|
+
total_tokens: 0,
|
|
99
|
+
prompt_tokens: 0,
|
|
100
|
+
completion_tokens: 0,
|
|
101
|
+
api_requests: 0,
|
|
102
|
+
successful_requests: 0,
|
|
103
|
+
failed_requests: 0,
|
|
104
|
+
usage_by_model: {},
|
|
105
|
+
usage_by_key: {},
|
|
106
|
+
daily_usage: [],
|
|
107
|
+
daily_by_model: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Transforms LiteLLM's SpendAnalyticsPaginatedResponse into the flatter
|
|
112
|
+
* UsageMetrics shape consumed by the frontend charts.
|
|
113
|
+
*
|
|
114
|
+
* Source shape (per result row):
|
|
115
|
+
* { date, metrics, breakdown: { models: { [name]: { metrics, api_key_breakdown: { [keyHash]: { metrics, metadata } } } } } }
|
|
116
|
+
*
|
|
117
|
+
* We fan that out into three views the UI consumes:
|
|
118
|
+
* - daily_usage → spend + request trends over time
|
|
119
|
+
* - usage_by_model → which models drove cost / traffic
|
|
120
|
+
* - usage_by_key → which keys drove cost / traffic (with key_alias + team_id from metadata)
|
|
121
|
+
*/
|
|
122
|
+
transformDailyActivity(response) {
|
|
123
|
+
const results = Array.isArray(response?.results) ? response.results : [];
|
|
124
|
+
const meta = response?.metadata ?? {};
|
|
125
|
+
const daily_usage = results
|
|
126
|
+
.map(r => ({
|
|
127
|
+
date: r.date,
|
|
128
|
+
spend: r.metrics?.spend ?? 0,
|
|
129
|
+
total_tokens: r.metrics?.total_tokens ?? 0,
|
|
130
|
+
prompt_tokens: r.metrics?.prompt_tokens ?? 0,
|
|
131
|
+
completion_tokens: r.metrics?.completion_tokens ?? 0,
|
|
132
|
+
api_requests: r.metrics?.api_requests ?? 0,
|
|
133
|
+
successful_requests: r.metrics?.successful_requests ?? 0,
|
|
134
|
+
failed_requests: r.metrics?.failed_requests ?? 0,
|
|
135
|
+
}))
|
|
136
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
137
|
+
const usage_by_model = {};
|
|
138
|
+
const usage_by_key = {};
|
|
139
|
+
const daily_by_model = [];
|
|
140
|
+
const emptyModelBucket = () => ({
|
|
141
|
+
total_spend: 0,
|
|
142
|
+
total_tokens: 0,
|
|
143
|
+
prompt_tokens: 0,
|
|
144
|
+
completion_tokens: 0,
|
|
145
|
+
api_requests: 0,
|
|
146
|
+
successful_requests: 0,
|
|
147
|
+
failed_requests: 0,
|
|
148
|
+
});
|
|
149
|
+
for (const r of results) {
|
|
150
|
+
const models = r.breakdown?.models ?? {};
|
|
151
|
+
for (const [name, entry] of Object.entries(models)) {
|
|
152
|
+
const m = entry?.metrics ?? {};
|
|
153
|
+
const bucket = usage_by_model[name] ?? emptyModelBucket();
|
|
154
|
+
bucket.total_spend += m.spend ?? 0;
|
|
155
|
+
bucket.total_tokens += m.total_tokens ?? 0;
|
|
156
|
+
bucket.prompt_tokens += m.prompt_tokens ?? 0;
|
|
157
|
+
bucket.completion_tokens += m.completion_tokens ?? 0;
|
|
158
|
+
bucket.api_requests += m.api_requests ?? 0;
|
|
159
|
+
bucket.successful_requests += m.successful_requests ?? 0;
|
|
160
|
+
bucket.failed_requests += m.failed_requests ?? 0;
|
|
161
|
+
usage_by_model[name] = bucket;
|
|
162
|
+
daily_by_model.push({
|
|
163
|
+
date: r.date,
|
|
164
|
+
model: name,
|
|
165
|
+
spend: m.spend ?? 0,
|
|
166
|
+
prompt_tokens: m.prompt_tokens ?? 0,
|
|
167
|
+
completion_tokens: m.completion_tokens ?? 0,
|
|
168
|
+
total_tokens: m.total_tokens ?? 0,
|
|
169
|
+
api_requests: m.api_requests ?? 0,
|
|
170
|
+
successful_requests: m.successful_requests ?? 0,
|
|
171
|
+
failed_requests: m.failed_requests ?? 0,
|
|
172
|
+
});
|
|
173
|
+
const keyMap = entry?.api_key_breakdown ?? {};
|
|
174
|
+
for (const [keyHash, keyEntry] of Object.entries(keyMap)) {
|
|
175
|
+
const km = keyEntry?.metrics ?? {};
|
|
176
|
+
const kmeta = keyEntry?.metadata ?? {};
|
|
177
|
+
const kb = usage_by_key[keyHash] ?? {
|
|
178
|
+
key_alias: kmeta.key_alias,
|
|
179
|
+
team_id: kmeta.team_id ?? null,
|
|
180
|
+
models: [],
|
|
181
|
+
...emptyModelBucket(),
|
|
182
|
+
};
|
|
183
|
+
if (!kb.key_alias && kmeta.key_alias)
|
|
184
|
+
kb.key_alias = kmeta.key_alias;
|
|
185
|
+
if (kb.team_id == null && kmeta.team_id)
|
|
186
|
+
kb.team_id = kmeta.team_id;
|
|
187
|
+
if (!kb.models.includes(name))
|
|
188
|
+
kb.models.push(name);
|
|
189
|
+
kb.total_spend += km.spend ?? 0;
|
|
190
|
+
kb.total_tokens += km.total_tokens ?? 0;
|
|
191
|
+
kb.prompt_tokens += km.prompt_tokens ?? 0;
|
|
192
|
+
kb.completion_tokens += km.completion_tokens ?? 0;
|
|
193
|
+
kb.api_requests += km.api_requests ?? 0;
|
|
194
|
+
kb.successful_requests += km.successful_requests ?? 0;
|
|
195
|
+
kb.failed_requests += km.failed_requests ?? 0;
|
|
196
|
+
usage_by_key[keyHash] = kb;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
total_spend: meta.total_spend ?? 0,
|
|
202
|
+
total_tokens: meta.total_tokens ?? 0,
|
|
203
|
+
prompt_tokens: meta.total_prompt_tokens ?? 0,
|
|
204
|
+
completion_tokens: meta.total_completion_tokens ?? 0,
|
|
205
|
+
api_requests: meta.total_api_requests ?? 0,
|
|
206
|
+
successful_requests: meta.total_successful_requests ?? 0,
|
|
207
|
+
failed_requests: meta.total_failed_requests ?? 0,
|
|
208
|
+
usage_by_model,
|
|
209
|
+
usage_by_key,
|
|
210
|
+
daily_usage,
|
|
211
|
+
daily_by_model,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async getUsage(startDate, endDate, userId, _groupBy) {
|
|
215
|
+
const params = new URLSearchParams({
|
|
216
|
+
start_date: startDate,
|
|
217
|
+
end_date: endDate,
|
|
218
|
+
page_size: '100',
|
|
219
|
+
});
|
|
220
|
+
if (userId)
|
|
221
|
+
params.append('user_id', userId);
|
|
222
|
+
try {
|
|
223
|
+
const response = await this.request(`/user/daily/activity?${params.toString()}`);
|
|
224
|
+
return this.transformDailyActivity(response);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
228
|
+
return this.emptyUsage();
|
|
229
|
+
}
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
234
|
+
const params = new URLSearchParams({
|
|
235
|
+
start_date: startDate,
|
|
236
|
+
end_date: endDate,
|
|
237
|
+
team_ids: teamId,
|
|
238
|
+
page_size: '100',
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
const response = await this.request(`/team/daily/activity?${params.toString()}`);
|
|
242
|
+
return this.transformDailyActivity(response);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (err.status === 404 || err.message.includes('not found')) {
|
|
246
|
+
return this.emptyUsage();
|
|
247
|
+
}
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
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
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -32,7 +22,6 @@ var index_exports = {};
|
|
|
32
22
|
__export(index_exports, {
|
|
33
23
|
LiteLLMClient: () => LiteLLMClient,
|
|
34
24
|
createRouter: () => createRouter,
|
|
35
|
-
default: () => litellmPlugin,
|
|
36
25
|
litellmPlugin: () => litellmPlugin
|
|
37
26
|
});
|
|
38
27
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -41,7 +30,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
41
30
|
var import_backend_plugin_api = require("@backstage/backend-plugin-api");
|
|
42
31
|
|
|
43
32
|
// src/router.ts
|
|
44
|
-
var import_express =
|
|
33
|
+
var import_express = require("express");
|
|
45
34
|
var import_catalog_client = require("@backstage/catalog-client");
|
|
46
35
|
|
|
47
36
|
// src/client.ts
|
|
@@ -61,15 +50,13 @@ var LiteLLMClient = class {
|
|
|
61
50
|
signal: controller.signal,
|
|
62
51
|
headers: {
|
|
63
52
|
"Content-Type": "application/json",
|
|
64
|
-
Authorization: `Bearer ${this.masterKey}`,
|
|
53
|
+
"Authorization": `Bearer ${this.masterKey}`,
|
|
65
54
|
...options.headers
|
|
66
55
|
}
|
|
67
56
|
});
|
|
68
57
|
if (!response.ok) {
|
|
69
58
|
const errorBody = await response.text();
|
|
70
|
-
const err = new Error(
|
|
71
|
-
`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`
|
|
72
|
-
);
|
|
59
|
+
const err = new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
73
60
|
err.status = response.status;
|
|
74
61
|
throw err;
|
|
75
62
|
}
|
|
@@ -97,36 +84,11 @@ var LiteLLMClient = class {
|
|
|
97
84
|
body: JSON.stringify(payload)
|
|
98
85
|
});
|
|
99
86
|
}
|
|
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
|
-
*/
|
|
122
87
|
async listKeys(userId) {
|
|
123
|
-
|
|
88
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
124
89
|
try {
|
|
125
|
-
const response = await this.request(
|
|
126
|
-
|
|
127
|
-
);
|
|
128
|
-
const rawKeys = response.keys ?? [];
|
|
129
|
-
return rawKeys.map(this.toVirtualKey);
|
|
90
|
+
const response = await this.request(`/key/info${query}`);
|
|
91
|
+
return Array.isArray(response) ? response : response.info ?? [];
|
|
130
92
|
} catch (err) {
|
|
131
93
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
132
94
|
return [];
|
|
@@ -134,44 +96,10 @@ var LiteLLMClient = class {
|
|
|
134
96
|
throw err;
|
|
135
97
|
}
|
|
136
98
|
}
|
|
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
|
-
*/
|
|
166
99
|
async generateKey(request) {
|
|
167
|
-
const { alias, ...rest } = request;
|
|
168
|
-
const payload = {
|
|
169
|
-
...rest,
|
|
170
|
-
...alias && { key_alias: alias }
|
|
171
|
-
};
|
|
172
100
|
return this.request("/key/generate", {
|
|
173
101
|
method: "POST",
|
|
174
|
-
body: JSON.stringify(
|
|
102
|
+
body: JSON.stringify({ json: request })
|
|
175
103
|
});
|
|
176
104
|
}
|
|
177
105
|
async updateKey(request) {
|
|
@@ -187,15 +115,11 @@ var LiteLLMClient = class {
|
|
|
187
115
|
});
|
|
188
116
|
}
|
|
189
117
|
async listModels() {
|
|
190
|
-
const response = await this.request(
|
|
191
|
-
"/models"
|
|
192
|
-
);
|
|
118
|
+
const response = await this.request("/models");
|
|
193
119
|
return Array.isArray(response) ? response : response.data ?? [];
|
|
194
120
|
}
|
|
195
121
|
async getTeamInfo(teamId) {
|
|
196
|
-
return this.request(
|
|
197
|
-
`/team/info?team_id=${encodeURIComponent(teamId)}`
|
|
198
|
-
);
|
|
122
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
199
123
|
}
|
|
200
124
|
emptyUsage() {
|
|
201
125
|
return {
|
|
@@ -319,9 +243,7 @@ var LiteLLMClient = class {
|
|
|
319
243
|
});
|
|
320
244
|
if (userId) params.append("user_id", userId);
|
|
321
245
|
try {
|
|
322
|
-
const response = await this.request(
|
|
323
|
-
`/user/daily/activity?${params.toString()}`
|
|
324
|
-
);
|
|
246
|
+
const response = await this.request(`/user/daily/activity?${params.toString()}`);
|
|
325
247
|
return this.transformDailyActivity(response);
|
|
326
248
|
} catch (err) {
|
|
327
249
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
@@ -338,9 +260,7 @@ var LiteLLMClient = class {
|
|
|
338
260
|
page_size: "100"
|
|
339
261
|
});
|
|
340
262
|
try {
|
|
341
|
-
const response = await this.request(
|
|
342
|
-
`/team/daily/activity?${params.toString()}`
|
|
343
|
-
);
|
|
263
|
+
const response = await this.request(`/team/daily/activity?${params.toString()}`);
|
|
344
264
|
return this.transformDailyActivity(response);
|
|
345
265
|
} catch (err) {
|
|
346
266
|
if (err.status === 404 || err.message.includes("not found")) {
|
|
@@ -450,7 +370,6 @@ async function provisionUser(client, userId, defaults, profile, backstageEntity,
|
|
|
450
370
|
...defaults.tpmLimit !== void 0 && { tpm_limit: defaults.tpmLimit },
|
|
451
371
|
...defaults.rpmLimit !== void 0 && { rpm_limit: defaults.rpmLimit },
|
|
452
372
|
...defaults.userRole && { user_role: defaults.userRole },
|
|
453
|
-
auto_create_key: false,
|
|
454
373
|
metadata: {
|
|
455
374
|
...defaults.metadata,
|
|
456
375
|
provisioned_by: "backstage",
|
|
@@ -618,7 +537,6 @@ async function createRouter(options) {
|
|
|
618
537
|
);
|
|
619
538
|
}
|
|
620
539
|
const router = (0, import_express.Router)();
|
|
621
|
-
router.use(import_express.default.json());
|
|
622
540
|
router.get("/health", (_req, res) => {
|
|
623
541
|
res.json({ status: "ok", provisioning: provisioningEnabled });
|
|
624
542
|
});
|
|
@@ -675,19 +593,6 @@ async function createRouter(options) {
|
|
|
675
593
|
});
|
|
676
594
|
router.post("/keys/generate", async (req, res) => {
|
|
677
595
|
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
|
-
}
|
|
691
596
|
const tokenEntityRef = await resolveUserId(req, auth);
|
|
692
597
|
const resolvedUserId = tokenEntityRef ? toLiteLLMUserId(tokenEntityRef, userIdDomain) : void 0;
|
|
693
598
|
if (resolvedUserId) {
|
|
@@ -703,20 +608,8 @@ async function createRouter(options) {
|
|
|
703
608
|
logger
|
|
704
609
|
);
|
|
705
610
|
}
|
|
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
|
-
};
|
|
717
611
|
const request = {
|
|
718
|
-
...body,
|
|
719
|
-
metadata: enrichedMetadata,
|
|
612
|
+
...req.body,
|
|
720
613
|
...resolvedUserId && { user_id: resolvedUserId }
|
|
721
614
|
};
|
|
722
615
|
const result = await client.generateKey(request);
|