@acarmisc/backstage-plugin-litellm-backend 0.1.0 → 0.1.2
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 +18 -0
- package/dist/client.js +75 -0
- package/dist/index.cjs.js +89 -19
- package/dist/index.cjs.js.map +2 -2
- package/dist/index.esm.js +250 -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/router.d.ts +9 -0
- package/dist/router.js +161 -0
- package/dist/types.cjs.js +1 -0
- package/dist/types.cjs.js.map +2 -2
- package/dist/types.d.ts +19 -3
- package/dist/types.js +2 -0
- package/package.json +2 -2
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } 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
|
+
getUserInfo(userId?: string): Promise<UserInfo>;
|
|
9
|
+
listKeys(userId?: string): Promise<VirtualKey[]>;
|
|
10
|
+
generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse>;
|
|
11
|
+
deleteKeys(request: DeleteKeyRequest): Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
listModels(): Promise<ModelInfo[]>;
|
|
15
|
+
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
16
|
+
getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics>;
|
|
17
|
+
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
18
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
throw new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
clearTimeout(timeoutId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async getUserInfo(userId) {
|
|
35
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
36
|
+
return this.request(`/user/info${query}`);
|
|
37
|
+
}
|
|
38
|
+
async listKeys(userId) {
|
|
39
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';
|
|
40
|
+
const response = await this.request(`/key/info${query}`);
|
|
41
|
+
return Array.isArray(response) ? response : (response.info ?? []);
|
|
42
|
+
}
|
|
43
|
+
async generateKey(request) {
|
|
44
|
+
return this.request('/key/generate', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: JSON.stringify(request),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async deleteKeys(request) {
|
|
50
|
+
return this.request('/key/delete', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
body: JSON.stringify(request),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async listModels() {
|
|
56
|
+
const response = await this.request('/models');
|
|
57
|
+
return Array.isArray(response) ? response : (response.data ?? []);
|
|
58
|
+
}
|
|
59
|
+
async getTeamInfo(teamId) {
|
|
60
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
61
|
+
}
|
|
62
|
+
async getUsage(startDate, endDate, userId, groupBy) {
|
|
63
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
64
|
+
if (userId)
|
|
65
|
+
params.append('user_id', userId);
|
|
66
|
+
if (groupBy)
|
|
67
|
+
params.append('group_by', groupBy);
|
|
68
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
69
|
+
}
|
|
70
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
71
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
72
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -67,7 +68,8 @@ var LiteLLMClient = class {
|
|
|
67
68
|
}
|
|
68
69
|
async listKeys(userId) {
|
|
69
70
|
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
70
|
-
|
|
71
|
+
const response = await this.request(`/key/info${query}`);
|
|
72
|
+
return Array.isArray(response) ? response : response.info ?? [];
|
|
71
73
|
}
|
|
72
74
|
async generateKey(request) {
|
|
73
75
|
return this.request("/key/generate", {
|
|
@@ -82,22 +84,40 @@ var LiteLLMClient = class {
|
|
|
82
84
|
});
|
|
83
85
|
}
|
|
84
86
|
async listModels() {
|
|
85
|
-
|
|
87
|
+
const response = await this.request("/models");
|
|
88
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
89
|
+
}
|
|
90
|
+
async getTeamInfo(teamId) {
|
|
91
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
86
92
|
}
|
|
87
93
|
async getUsage(startDate, endDate, userId, groupBy) {
|
|
88
|
-
const params = new URLSearchParams({
|
|
89
|
-
start_date: startDate,
|
|
90
|
-
end_date: endDate
|
|
91
|
-
});
|
|
94
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
92
95
|
if (userId) params.append("user_id", userId);
|
|
93
96
|
if (groupBy) params.append("group_by", groupBy);
|
|
94
97
|
return this.request(`/usage/keys?${params.toString()}`);
|
|
95
98
|
}
|
|
99
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
100
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
101
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
102
|
+
}
|
|
96
103
|
};
|
|
97
104
|
|
|
98
105
|
// src/router.ts
|
|
106
|
+
async function resolveUserId(req, auth) {
|
|
107
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
108
|
+
if (!rawToken) return void 0;
|
|
109
|
+
try {
|
|
110
|
+
const credentials = await auth.authenticate(rawToken);
|
|
111
|
+
const principal = credentials.principal;
|
|
112
|
+
if (principal?.type === "user") {
|
|
113
|
+
return principal.userEntityRef;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
99
119
|
async function createRouter(options) {
|
|
100
|
-
const { config, logger } = options;
|
|
120
|
+
const { config, logger, auth } = options;
|
|
101
121
|
const baseUrl = config.getString("litellm.baseUrl");
|
|
102
122
|
const masterKey = config.getString("litellm.masterKey");
|
|
103
123
|
const client = new LiteLLMClient({ baseUrl, masterKey });
|
|
@@ -107,7 +127,8 @@ async function createRouter(options) {
|
|
|
107
127
|
});
|
|
108
128
|
router.get("/user/info", async (req, res) => {
|
|
109
129
|
try {
|
|
110
|
-
const
|
|
130
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
131
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
111
132
|
const userInfo = await client.getUserInfo(userId);
|
|
112
133
|
res.json(userInfo);
|
|
113
134
|
} catch (error) {
|
|
@@ -117,7 +138,8 @@ async function createRouter(options) {
|
|
|
117
138
|
});
|
|
118
139
|
router.get("/keys", async (req, res) => {
|
|
119
140
|
try {
|
|
120
|
-
const
|
|
141
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
142
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
121
143
|
const keys = await client.listKeys(userId);
|
|
122
144
|
res.json(keys);
|
|
123
145
|
} catch (error) {
|
|
@@ -127,7 +149,12 @@ async function createRouter(options) {
|
|
|
127
149
|
});
|
|
128
150
|
router.post("/keys/generate", async (req, res) => {
|
|
129
151
|
try {
|
|
130
|
-
const
|
|
152
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
153
|
+
const request = {
|
|
154
|
+
...req.body,
|
|
155
|
+
// Bind generated key to the authenticated user so LiteLLM enforces their limits.
|
|
156
|
+
...tokenUserId && { user_id: tokenUserId }
|
|
157
|
+
};
|
|
131
158
|
const result = await client.generateKey(request);
|
|
132
159
|
res.json(result);
|
|
133
160
|
} catch (error) {
|
|
@@ -158,6 +185,48 @@ async function createRouter(options) {
|
|
|
158
185
|
res.status(500).json({ error: error.message });
|
|
159
186
|
}
|
|
160
187
|
});
|
|
188
|
+
router.get("/teams", async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
191
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
192
|
+
const userInfo = await client.getUserInfo(userId);
|
|
193
|
+
if (!userInfo.teams?.length) {
|
|
194
|
+
res.json([]);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const teams = await Promise.all(
|
|
198
|
+
userInfo.teams.map(
|
|
199
|
+
(teamId) => client.getTeamInfo(teamId).catch((err) => {
|
|
200
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
201
|
+
return null;
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
res.json(teams.filter(Boolean));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logger.error("Failed to fetch teams", error);
|
|
208
|
+
res.status(500).json({ error: error.message });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
router.get("/teams/:teamId/usage", async (req, res) => {
|
|
212
|
+
try {
|
|
213
|
+
const { teamId } = req.params;
|
|
214
|
+
const { start_date, end_date } = req.query;
|
|
215
|
+
if (!start_date || !end_date) {
|
|
216
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const usage = await client.getTeamUsage(
|
|
220
|
+
teamId,
|
|
221
|
+
start_date,
|
|
222
|
+
end_date
|
|
223
|
+
);
|
|
224
|
+
res.json(usage);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logger.error("Failed to fetch team usage", error);
|
|
227
|
+
res.status(500).json({ error: error.message });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
161
230
|
router.get("/usage", async (req, res) => {
|
|
162
231
|
try {
|
|
163
232
|
const { start_date, end_date, user_id, group_by } = req.query;
|
|
@@ -165,10 +234,12 @@ async function createRouter(options) {
|
|
|
165
234
|
res.status(400).json({ error: "start_date and end_date are required" });
|
|
166
235
|
return;
|
|
167
236
|
}
|
|
237
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
238
|
+
const userId = tokenUserId ?? user_id;
|
|
168
239
|
const usage = await client.getUsage(
|
|
169
240
|
start_date,
|
|
170
241
|
end_date,
|
|
171
|
-
|
|
242
|
+
userId,
|
|
172
243
|
group_by
|
|
173
244
|
);
|
|
174
245
|
res.json(usage);
|
|
@@ -186,15 +257,14 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
186
257
|
register(reg) {
|
|
187
258
|
reg.registerInit({
|
|
188
259
|
deps: {
|
|
189
|
-
httpRouter:
|
|
190
|
-
config:
|
|
191
|
-
logger:
|
|
260
|
+
httpRouter: import_backend_plugin_api.coreServices.httpRouter,
|
|
261
|
+
config: import_backend_plugin_api.coreServices.rootConfig,
|
|
262
|
+
logger: import_backend_plugin_api.coreServices.logger,
|
|
263
|
+
auth: import_backend_plugin_api.coreServices.auth,
|
|
264
|
+
discovery: import_backend_plugin_api.coreServices.discovery
|
|
192
265
|
},
|
|
193
|
-
async init({ httpRouter, config, logger }) {
|
|
194
|
-
const router = await createRouter({
|
|
195
|
-
config,
|
|
196
|
-
logger
|
|
197
|
-
});
|
|
266
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
267
|
+
const router = await createRouter({ config, logger, auth });
|
|
198
268
|
httpRouter.use(router);
|
|
199
269
|
}
|
|
200
270
|
});
|
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 { createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg: any) {\n reg.registerInit({\n deps: {\n httpRouter: 'coreServices.httpRouter',\n config: 'coreServices.rootConfig',\n logger: 'coreServices.logger',\n },\n async init({ httpRouter, config, logger }) {\n const router = await createRouter({\n config,\n logger,\n });\n httpRouter.use(router);\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 return this.request<VirtualKey[]>(`/key/info${query}`);\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 return this.request<ModelInfo[]>('/models');\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}"],
|
|
5
|
-
"mappings": "
|
|
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} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\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 if\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); // strip \"Bearer \"\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 // token invalid or service token \u2014 fall through\n }\n return undefined;\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\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n // Resolve user: prefer the identity extracted from the Backstage token so the\n // caller cannot spoof another user_id. Falls back to the query param only when\n // no user token is present (e.g. admin tooling using a service token).\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 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 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 // Bind generated key to the authenticated user so LiteLLM enforces their limits.\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 // Returns TeamInfo for every team the authenticated user belongs to.\n // Team membership is read from /user/info .teams[], then each team is\n // resolved in parallel via /team/info.\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 = 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, 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 tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (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} 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 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,250 @@
|
|
|
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
|
+
throw new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timeoutId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async getUserInfo(userId) {
|
|
38
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
39
|
+
return this.request(`/user/info${query}`);
|
|
40
|
+
}
|
|
41
|
+
async listKeys(userId) {
|
|
42
|
+
const query = userId ? `?user_id=${encodeURIComponent(userId)}` : "";
|
|
43
|
+
const response = await this.request(`/key/info${query}`);
|
|
44
|
+
return Array.isArray(response) ? response : response.info ?? [];
|
|
45
|
+
}
|
|
46
|
+
async generateKey(request) {
|
|
47
|
+
return this.request("/key/generate", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
body: JSON.stringify(request)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async deleteKeys(request) {
|
|
53
|
+
return this.request("/key/delete", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
body: JSON.stringify(request)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async listModels() {
|
|
59
|
+
const response = await this.request("/models");
|
|
60
|
+
return Array.isArray(response) ? response : response.data ?? [];
|
|
61
|
+
}
|
|
62
|
+
async getTeamInfo(teamId) {
|
|
63
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
64
|
+
}
|
|
65
|
+
async getUsage(startDate, endDate, userId, groupBy) {
|
|
66
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
67
|
+
if (userId) params.append("user_id", userId);
|
|
68
|
+
if (groupBy) params.append("group_by", groupBy);
|
|
69
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
70
|
+
}
|
|
71
|
+
async getTeamUsage(teamId, startDate, endDate) {
|
|
72
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate, team_id: teamId });
|
|
73
|
+
return this.request(`/usage/keys?${params.toString()}`);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/router.ts
|
|
78
|
+
async function resolveUserId(req, auth) {
|
|
79
|
+
const rawToken = req.headers.authorization?.slice(7);
|
|
80
|
+
if (!rawToken) return void 0;
|
|
81
|
+
try {
|
|
82
|
+
const credentials = await auth.authenticate(rawToken);
|
|
83
|
+
const principal = credentials.principal;
|
|
84
|
+
if (principal?.type === "user") {
|
|
85
|
+
return principal.userEntityRef;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
async function createRouter(options) {
|
|
92
|
+
const { config, logger, auth } = options;
|
|
93
|
+
const baseUrl = config.getString("litellm.baseUrl");
|
|
94
|
+
const masterKey = config.getString("litellm.masterKey");
|
|
95
|
+
const client = new LiteLLMClient({ baseUrl, masterKey });
|
|
96
|
+
const router = Router();
|
|
97
|
+
router.get("/health", (_req, res) => {
|
|
98
|
+
res.json({ status: "ok" });
|
|
99
|
+
});
|
|
100
|
+
router.get("/user/info", async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
103
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
104
|
+
const userInfo = await client.getUserInfo(userId);
|
|
105
|
+
res.json(userInfo);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error("Failed to fetch user info", error);
|
|
108
|
+
res.status(500).json({ error: error.message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
router.get("/keys", async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
114
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
115
|
+
const keys = await client.listKeys(userId);
|
|
116
|
+
res.json(keys);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error("Failed to list keys", error);
|
|
119
|
+
res.status(500).json({ error: error.message });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
router.post("/keys/generate", async (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
125
|
+
const request = {
|
|
126
|
+
...req.body,
|
|
127
|
+
// Bind generated key to the authenticated user so LiteLLM enforces their limits.
|
|
128
|
+
...tokenUserId && { user_id: tokenUserId }
|
|
129
|
+
};
|
|
130
|
+
const result = await client.generateKey(request);
|
|
131
|
+
res.json(result);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
logger.error("Failed to generate key", error);
|
|
134
|
+
res.status(500).json({ error: error.message });
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
router.delete("/keys/:keyId", async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const { keyId } = req.params;
|
|
140
|
+
if (!keyId) {
|
|
141
|
+
res.status(400).json({ error: "keyId is required" });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await client.deleteKeys({ keys: [keyId] });
|
|
145
|
+
res.json({ success: true });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error("Failed to delete key", error);
|
|
148
|
+
res.status(500).json({ error: error.message });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
router.get("/models", async (_req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
const models = await client.listModels();
|
|
154
|
+
res.json(models);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
logger.error("Failed to list models", error);
|
|
157
|
+
res.status(500).json({ error: error.message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
router.get("/teams", async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
163
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
164
|
+
const userInfo = await client.getUserInfo(userId);
|
|
165
|
+
if (!userInfo.teams?.length) {
|
|
166
|
+
res.json([]);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const teams = await Promise.all(
|
|
170
|
+
userInfo.teams.map(
|
|
171
|
+
(teamId) => client.getTeamInfo(teamId).catch((err) => {
|
|
172
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
173
|
+
return null;
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
res.json(teams.filter(Boolean));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logger.error("Failed to fetch teams", error);
|
|
180
|
+
res.status(500).json({ error: error.message });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
router.get("/teams/:teamId/usage", async (req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
const { teamId } = req.params;
|
|
186
|
+
const { start_date, end_date } = req.query;
|
|
187
|
+
if (!start_date || !end_date) {
|
|
188
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const usage = await client.getTeamUsage(
|
|
192
|
+
teamId,
|
|
193
|
+
start_date,
|
|
194
|
+
end_date
|
|
195
|
+
);
|
|
196
|
+
res.json(usage);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.error("Failed to fetch team usage", error);
|
|
199
|
+
res.status(500).json({ error: error.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
router.get("/usage", async (req, res) => {
|
|
203
|
+
try {
|
|
204
|
+
const { start_date, end_date, user_id, group_by } = req.query;
|
|
205
|
+
if (!start_date || !end_date) {
|
|
206
|
+
res.status(400).json({ error: "start_date and end_date are required" });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
210
|
+
const userId = tokenUserId ?? user_id;
|
|
211
|
+
const usage = await client.getUsage(
|
|
212
|
+
start_date,
|
|
213
|
+
end_date,
|
|
214
|
+
userId,
|
|
215
|
+
group_by
|
|
216
|
+
);
|
|
217
|
+
res.json(usage);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.error("Failed to fetch usage", error);
|
|
220
|
+
res.status(500).json({ error: error.message });
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return router;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/plugin.ts
|
|
227
|
+
var litellmPlugin = createBackendPlugin({
|
|
228
|
+
pluginId: "litellm",
|
|
229
|
+
register(reg) {
|
|
230
|
+
reg.registerInit({
|
|
231
|
+
deps: {
|
|
232
|
+
httpRouter: coreServices.httpRouter,
|
|
233
|
+
config: coreServices.rootConfig,
|
|
234
|
+
logger: coreServices.logger,
|
|
235
|
+
auth: coreServices.auth,
|
|
236
|
+
discovery: coreServices.discovery
|
|
237
|
+
},
|
|
238
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
239
|
+
const router = await createRouter({ config, logger, auth });
|
|
240
|
+
httpRouter.use(router);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
export {
|
|
246
|
+
LiteLLMClient,
|
|
247
|
+
createRouter,
|
|
248
|
+
litellmPlugin
|
|
249
|
+
};
|
|
250
|
+
//# 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} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n auth: AuthService;\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 if\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); // strip \"Bearer \"\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 // token invalid or service token \u2014 fall through\n }\n return undefined;\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\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n // Resolve user: prefer the identity extracted from the Backstage token so the\n // caller cannot spoof another user_id. Falls back to the query param only when\n // no user token is present (e.g. admin tooling using a service token).\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 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 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 // Bind generated key to the authenticated user so LiteLLM enforces their limits.\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 // Returns TeamInfo for every team the authenticated user belongs to.\n // Team membership is read from /user/info .teams[], then each team is\n // resolved in parallel via /team/info.\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 = 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, 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 tokenUserId = await resolveUserId(req, auth);\n const userId = tokenUserId ?? (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} 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 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/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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.litellmPlugin = void 0;
|
|
18
|
+
var plugin_1 = require("./plugin");
|
|
19
|
+
Object.defineProperty(exports, "litellmPlugin", { enumerable: true, get: function () { return plugin_1.litellmPlugin; } });
|
|
20
|
+
var router_1 = require("./router");
|
|
21
|
+
Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return router_1.createRouter; } });
|
|
22
|
+
__exportStar(require("./types"), exports);
|
|
23
|
+
var client_1 = require("./client");
|
|
24
|
+
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 }) {
|
|
18
|
+
const router = await (0, router_1.createRouter)({ config, logger, auth });
|
|
19
|
+
httpRouter.use(router);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
});
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { Config } from '@backstage/config';
|
|
3
|
+
import { AuthService } from '@backstage/backend-plugin-api';
|
|
4
|
+
export interface RouterOptions {
|
|
5
|
+
config: Config;
|
|
6
|
+
logger: any;
|
|
7
|
+
auth: AuthService;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRouter(options: RouterOptions): Promise<Router>;
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRouter = createRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const client_1 = require("./client");
|
|
6
|
+
/**
|
|
7
|
+
* Extracts the authenticated Backstage user identity from the request token.
|
|
8
|
+
* Returns the userEntityRef (e.g. "user:default/john.doe") or undefined if
|
|
9
|
+
* the request carries no user credential (service-to-service calls).
|
|
10
|
+
*/
|
|
11
|
+
async function resolveUserId(req, auth) {
|
|
12
|
+
const rawToken = req.headers.authorization?.slice(7); // strip "Bearer "
|
|
13
|
+
if (!rawToken)
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const credentials = await auth.authenticate(rawToken);
|
|
17
|
+
const principal = credentials.principal;
|
|
18
|
+
if (principal?.type === 'user') {
|
|
19
|
+
return principal.userEntityRef;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// token invalid or service token — fall through
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
async function createRouter(options) {
|
|
28
|
+
const { config, logger, auth } = options;
|
|
29
|
+
const baseUrl = config.getString('litellm.baseUrl');
|
|
30
|
+
const masterKey = config.getString('litellm.masterKey');
|
|
31
|
+
const client = new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
32
|
+
const router = (0, express_1.Router)();
|
|
33
|
+
router.get('/health', (_req, res) => {
|
|
34
|
+
res.json({ status: 'ok' });
|
|
35
|
+
});
|
|
36
|
+
// Resolve user: prefer the identity extracted from the Backstage token so the
|
|
37
|
+
// caller cannot spoof another user_id. Falls back to the query param only when
|
|
38
|
+
// no user token is present (e.g. admin tooling using a service token).
|
|
39
|
+
router.get('/user/info', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
42
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
43
|
+
const userInfo = await client.getUserInfo(userId);
|
|
44
|
+
res.json(userInfo);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.error('Failed to fetch user info', error);
|
|
48
|
+
res.status(500).json({ error: error.message });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
router.get('/keys', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
54
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
55
|
+
const keys = await client.listKeys(userId);
|
|
56
|
+
res.json(keys);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error('Failed to list keys', error);
|
|
60
|
+
res.status(500).json({ error: error.message });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
router.post('/keys/generate', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
66
|
+
const request = {
|
|
67
|
+
...req.body,
|
|
68
|
+
// Bind generated key to the authenticated user so LiteLLM enforces their limits.
|
|
69
|
+
...(tokenUserId && { user_id: tokenUserId }),
|
|
70
|
+
};
|
|
71
|
+
const result = await client.generateKey(request);
|
|
72
|
+
res.json(result);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
logger.error('Failed to generate key', error);
|
|
76
|
+
res.status(500).json({ error: error.message });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
router.delete('/keys/:keyId', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { keyId } = req.params;
|
|
82
|
+
if (!keyId) {
|
|
83
|
+
res.status(400).json({ error: 'keyId is required' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await client.deleteKeys({ keys: [keyId] });
|
|
87
|
+
res.json({ success: true });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error('Failed to delete key', error);
|
|
91
|
+
res.status(500).json({ error: error.message });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
router.get('/models', async (_req, res) => {
|
|
95
|
+
try {
|
|
96
|
+
const models = await client.listModels();
|
|
97
|
+
res.json(models);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('Failed to list models', error);
|
|
101
|
+
res.status(500).json({ error: error.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Returns TeamInfo for every team the authenticated user belongs to.
|
|
105
|
+
// Team membership is read from /user/info .teams[], then each team is
|
|
106
|
+
// resolved in parallel via /team/info.
|
|
107
|
+
router.get('/teams', async (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
110
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
111
|
+
const userInfo = await client.getUserInfo(userId);
|
|
112
|
+
if (!userInfo.teams?.length) {
|
|
113
|
+
res.json([]);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const teams = await Promise.all(userInfo.teams.map(teamId => client.getTeamInfo(teamId).catch(err => {
|
|
117
|
+
logger.warn(`Failed to fetch team ${teamId}: ${err.message}`);
|
|
118
|
+
return null;
|
|
119
|
+
})));
|
|
120
|
+
res.json(teams.filter(Boolean));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error('Failed to fetch teams', error);
|
|
124
|
+
res.status(500).json({ error: error.message });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
router.get('/teams/:teamId/usage', async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const { teamId } = req.params;
|
|
130
|
+
const { start_date, end_date } = req.query;
|
|
131
|
+
if (!start_date || !end_date) {
|
|
132
|
+
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const usage = await client.getTeamUsage(teamId, start_date, end_date);
|
|
136
|
+
res.json(usage);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error('Failed to fetch team usage', error);
|
|
140
|
+
res.status(500).json({ error: error.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
router.get('/usage', async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const { start_date, end_date, user_id, group_by } = req.query;
|
|
146
|
+
if (!start_date || !end_date) {
|
|
147
|
+
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
151
|
+
const userId = tokenUserId ?? user_id;
|
|
152
|
+
const usage = await client.getUsage(start_date, end_date, userId, group_by);
|
|
153
|
+
res.json(usage);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger.error('Failed to fetch usage', error);
|
|
157
|
+
res.status(500).json({ error: error.message });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return router;
|
|
161
|
+
}
|
package/dist/types.cjs.js
CHANGED
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
|
|
5
|
-
"mappings": "
|
|
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"],
|
|
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;
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acarmisc/backstage-plugin-litellm-backend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The Backstage backend plugin for LiteLLM governance",
|
|
5
5
|
"backstage": {
|
|
6
6
|
"role": "backend-plugin",
|
|
7
7
|
"pluginId": "litellm",
|
|
8
8
|
"pluginPackages": [
|
|
9
|
-
"@
|
|
9
|
+
"@acarmisc/backstage-plugin-litellm-backend"
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|