@acarmisc/backstage-plugin-litellm-backend 0.1.1 → 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 +3 -1
- package/dist/client.js +8 -4
- package/dist/index.cjs.js +79 -11
- 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/plugin.js +2 -2
- package/dist/router.d.ts +2 -0
- package/dist/router.js +77 -5
- package/dist/types.cjs.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } from './types';
|
|
1
|
+
import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, TeamInfo, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } from './types';
|
|
2
2
|
export declare class LiteLLMClient {
|
|
3
3
|
private baseUrl;
|
|
4
4
|
private masterKey;
|
|
@@ -12,5 +12,7 @@ export declare class LiteLLMClient {
|
|
|
12
12
|
success: boolean;
|
|
13
13
|
}>;
|
|
14
14
|
listModels(): Promise<ModelInfo[]>;
|
|
15
|
+
getTeamInfo(teamId: string): Promise<TeamInfo>;
|
|
15
16
|
getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics>;
|
|
17
|
+
getTeamUsage(teamId: string, startDate: string, endDate: string): Promise<UsageMetrics>;
|
|
16
18
|
}
|
package/dist/client.js
CHANGED
|
@@ -56,16 +56,20 @@ class LiteLLMClient {
|
|
|
56
56
|
const response = await this.request('/models');
|
|
57
57
|
return Array.isArray(response) ? response : (response.data ?? []);
|
|
58
58
|
}
|
|
59
|
+
async getTeamInfo(teamId) {
|
|
60
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
61
|
+
}
|
|
59
62
|
async getUsage(startDate, endDate, userId, groupBy) {
|
|
60
|
-
const params = new URLSearchParams({
|
|
61
|
-
start_date: startDate,
|
|
62
|
-
end_date: endDate,
|
|
63
|
-
});
|
|
63
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
64
64
|
if (userId)
|
|
65
65
|
params.append('user_id', userId);
|
|
66
66
|
if (groupBy)
|
|
67
67
|
params.append('group_by', groupBy);
|
|
68
68
|
return this.request(`/usage/keys?${params.toString()}`);
|
|
69
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
|
+
}
|
|
70
74
|
}
|
|
71
75
|
exports.LiteLLMClient = LiteLLMClient;
|
package/dist/index.cjs.js
CHANGED
|
@@ -87,20 +87,37 @@ var LiteLLMClient = class {
|
|
|
87
87
|
const response = await this.request("/models");
|
|
88
88
|
return Array.isArray(response) ? response : response.data ?? [];
|
|
89
89
|
}
|
|
90
|
+
async getTeamInfo(teamId) {
|
|
91
|
+
return this.request(`/team/info?team_id=${encodeURIComponent(teamId)}`);
|
|
92
|
+
}
|
|
90
93
|
async getUsage(startDate, endDate, userId, groupBy) {
|
|
91
|
-
const params = new URLSearchParams({
|
|
92
|
-
start_date: startDate,
|
|
93
|
-
end_date: endDate
|
|
94
|
-
});
|
|
94
|
+
const params = new URLSearchParams({ start_date: startDate, end_date: endDate });
|
|
95
95
|
if (userId) params.append("user_id", userId);
|
|
96
96
|
if (groupBy) params.append("group_by", groupBy);
|
|
97
97
|
return this.request(`/usage/keys?${params.toString()}`);
|
|
98
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
|
+
}
|
|
99
103
|
};
|
|
100
104
|
|
|
101
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
|
+
}
|
|
102
119
|
async function createRouter(options) {
|
|
103
|
-
const { config, logger } = options;
|
|
120
|
+
const { config, logger, auth } = options;
|
|
104
121
|
const baseUrl = config.getString("litellm.baseUrl");
|
|
105
122
|
const masterKey = config.getString("litellm.masterKey");
|
|
106
123
|
const client = new LiteLLMClient({ baseUrl, masterKey });
|
|
@@ -110,7 +127,8 @@ async function createRouter(options) {
|
|
|
110
127
|
});
|
|
111
128
|
router.get("/user/info", async (req, res) => {
|
|
112
129
|
try {
|
|
113
|
-
const
|
|
130
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
131
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
114
132
|
const userInfo = await client.getUserInfo(userId);
|
|
115
133
|
res.json(userInfo);
|
|
116
134
|
} catch (error) {
|
|
@@ -120,7 +138,8 @@ async function createRouter(options) {
|
|
|
120
138
|
});
|
|
121
139
|
router.get("/keys", async (req, res) => {
|
|
122
140
|
try {
|
|
123
|
-
const
|
|
141
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
142
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
124
143
|
const keys = await client.listKeys(userId);
|
|
125
144
|
res.json(keys);
|
|
126
145
|
} catch (error) {
|
|
@@ -130,7 +149,12 @@ async function createRouter(options) {
|
|
|
130
149
|
});
|
|
131
150
|
router.post("/keys/generate", async (req, res) => {
|
|
132
151
|
try {
|
|
133
|
-
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
|
+
};
|
|
134
158
|
const result = await client.generateKey(request);
|
|
135
159
|
res.json(result);
|
|
136
160
|
} catch (error) {
|
|
@@ -161,6 +185,48 @@ async function createRouter(options) {
|
|
|
161
185
|
res.status(500).json({ error: error.message });
|
|
162
186
|
}
|
|
163
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
|
+
});
|
|
164
230
|
router.get("/usage", async (req, res) => {
|
|
165
231
|
try {
|
|
166
232
|
const { start_date, end_date, user_id, group_by } = req.query;
|
|
@@ -168,10 +234,12 @@ async function createRouter(options) {
|
|
|
168
234
|
res.status(400).json({ error: "start_date and end_date are required" });
|
|
169
235
|
return;
|
|
170
236
|
}
|
|
237
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
238
|
+
const userId = tokenUserId ?? user_id;
|
|
171
239
|
const usage = await client.getUsage(
|
|
172
240
|
start_date,
|
|
173
241
|
end_date,
|
|
174
|
-
|
|
242
|
+
userId,
|
|
175
243
|
group_by
|
|
176
244
|
);
|
|
177
245
|
res.json(usage);
|
|
@@ -195,8 +263,8 @@ var litellmPlugin = (0, import_backend_plugin_api.createBackendPlugin)({
|
|
|
195
263
|
auth: import_backend_plugin_api.coreServices.auth,
|
|
196
264
|
discovery: import_backend_plugin_api.coreServices.discovery
|
|
197
265
|
},
|
|
198
|
-
async init({ httpRouter, config, logger }) {
|
|
199
|
-
const router = await createRouter({ config, logger });
|
|
266
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
267
|
+
const router = await createRouter({ config, logger, auth });
|
|
200
268
|
httpRouter.use(router);
|
|
201
269
|
}
|
|
202
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 { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger }) {\n const router = await createRouter({ config, logger });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n GenerateKeyRequest,\n GenerateKeyResponse,\n} from './types';\n\nexport interface RouterOptions {\n config: Config;\n logger: any;\n}\n\nexport async function createRouter(options: RouterOptions): Promise<Router> {\n const { config, logger } = options;\n\n const baseUrl = config.getString('litellm.baseUrl');\n const masterKey = config.getString('litellm.masterKey');\n const client = new LiteLLMClient({ baseUrl, masterKey });\n\n const router = Router();\n\n router.get('/health', (_req: Request, res: Response) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/user/info', async (req: Request, res: Response) => {\n try {\n const userId = req.query.user_id as string | undefined;\n const userInfo: UserInfo = await client.getUserInfo(userId);\n res.json(userInfo);\n } catch (error: any) {\n logger.error('Failed to fetch user info', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/keys', async (req: Request, res: Response) => {\n try {\n const userId = req.query.user_id as string | undefined;\n const keys: VirtualKey[] = await client.listKeys(userId);\n res.json(keys);\n } catch (error: any) {\n logger.error('Failed to list keys', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.post('/keys/generate', async (req: Request, res: Response) => {\n try {\n const request: GenerateKeyRequest = req.body;\n const result: GenerateKeyResponse = await client.generateKey(request);\n res.json(result);\n } catch (error: any) {\n logger.error('Failed to generate key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.delete('/keys/:keyId', async (req: Request, res: Response) => {\n try {\n const { keyId } = req.params;\n if (!keyId) {\n res.status(400).json({ error: 'keyId is required' });\n return;\n }\n await client.deleteKeys({ keys: [keyId] });\n res.json({ success: true });\n } catch (error: any) {\n logger.error('Failed to delete key', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/models', async (_req: Request, res: Response) => {\n try {\n const models: ModelInfo[] = await client.listModels();\n res.json(models);\n } catch (error: any) {\n logger.error('Failed to list models', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n router.get('/usage', async (req: Request, res: Response) => {\n try {\n const { start_date, end_date, user_id, group_by } = req.query;\n if (!start_date || !end_date) {\n res.status(400).json({ error: 'start_date and end_date are required' });\n return;\n }\n const usage: UsageMetrics = await client.getUsage(\n start_date as string,\n end_date as string,\n user_id as string | undefined,\n group_by as string | undefined\n );\n res.json(usage);\n } catch (error: any) {\n logger.error('Failed to fetch usage', error);\n res.status(500).json({ error: error.message });\n }\n });\n\n return router;\n}", "import { LiteLLMConfig, UserInfo, VirtualKey, ModelInfo, UsageMetrics, GenerateKeyRequest, GenerateKeyResponse, DeleteKeyRequest } from './types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\nexport class LiteLLMClient {\n private baseUrl: string;\n private masterKey: string;\n private timeout: number;\n\n constructor(config: LiteLLMConfig, timeout = DEFAULT_TIMEOUT) {\n this.baseUrl = config.baseUrl.replace(/\\/$/, '');\n this.masterKey = config.masterKey;\n this.timeout = timeout;\n }\n\n private async request<T>(path: string, options: RequestInit = {}): Promise<T> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.timeout);\n\n try {\n const response = await fetch(`${this.baseUrl}${path}`, {\n ...options,\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.masterKey}`,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(`LiteLLM API error: ${response.status} ${response.statusText} - ${errorBody}`);\n }\n\n return response.json();\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n async getUserInfo(userId?: string): Promise<UserInfo> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n return this.request<UserInfo>(`/user/info${query}`);\n }\n\n async listKeys(userId?: string): Promise<VirtualKey[]> {\n const query = userId ? `?user_id=${encodeURIComponent(userId)}` : '';\n const response = await this.request<{ info: VirtualKey[] } | VirtualKey[]>(`/key/info${query}`);\n return Array.isArray(response) ? response : (response.info ?? []);\n }\n\n async generateKey(request: GenerateKeyRequest): Promise<GenerateKeyResponse> {\n return this.request<GenerateKeyResponse>('/key/generate', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async deleteKeys(request: DeleteKeyRequest): Promise<{ success: boolean }> {\n return this.request<{ success: boolean }>('/key/delete', {\n method: 'POST',\n body: JSON.stringify(request),\n });\n }\n\n async listModels(): Promise<ModelInfo[]> {\n const response = await this.request<{ data: ModelInfo[] } | ModelInfo[]>('/models');\n return Array.isArray(response) ? response : (response.data ?? []);\n }\n\n async getUsage(startDate: string, endDate: string, userId?: string, groupBy?: string): Promise<UsageMetrics> {\n const params = new URLSearchParams({\n start_date: startDate,\n end_date: endDate,\n });\n if (userId) params.append('user_id', userId);\n if (groupBy) params.append('group_by', groupBy);\n\n return this.request<UsageMetrics>(`/usage/keys?${params.toString()}`);\n }\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,gCAAkD;;;ACAlD,qBAA0C;;;
|
|
4
|
+
"sourcesContent": ["export { litellmPlugin } from './plugin';\nexport { createRouter } from './router';\nexport * from './types';\nexport { LiteLLMClient } from './client';", "import { coreServices, createBackendPlugin } from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\n\nexport const litellmPlugin = createBackendPlugin({\n pluginId: 'litellm',\n register(reg) {\n reg.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n config: coreServices.rootConfig,\n logger: coreServices.logger,\n auth: coreServices.auth,\n discovery: coreServices.discovery,\n },\n async init({ httpRouter, config, logger, auth }) {\n const router = await createRouter({ config, logger, auth });\n httpRouter.use(router);\n },\n });\n },\n});\n", "import { Router, Request, Response } from 'express';\nimport { Config } from '@backstage/config';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { LiteLLMClient } from './client';\nimport {\n UserInfo,\n VirtualKey,\n ModelInfo,\n UsageMetrics,\n TeamInfo,\n GenerateKeyRequest,\n GenerateKeyResponse,\n} 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/plugin.js
CHANGED
|
@@ -14,8 +14,8 @@ exports.litellmPlugin = (0, backend_plugin_api_1.createBackendPlugin)({
|
|
|
14
14
|
auth: backend_plugin_api_1.coreServices.auth,
|
|
15
15
|
discovery: backend_plugin_api_1.coreServices.discovery,
|
|
16
16
|
},
|
|
17
|
-
async init({ httpRouter, config, logger }) {
|
|
18
|
-
const router = await (0, router_1.createRouter)({ config, logger });
|
|
17
|
+
async init({ httpRouter, config, logger, auth }) {
|
|
18
|
+
const router = await (0, router_1.createRouter)({ config, logger, auth });
|
|
19
19
|
httpRouter.use(router);
|
|
20
20
|
},
|
|
21
21
|
});
|
package/dist/router.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { Config } from '@backstage/config';
|
|
3
|
+
import { AuthService } from '@backstage/backend-plugin-api';
|
|
3
4
|
export interface RouterOptions {
|
|
4
5
|
config: Config;
|
|
5
6
|
logger: any;
|
|
7
|
+
auth: AuthService;
|
|
6
8
|
}
|
|
7
9
|
export declare function createRouter(options: RouterOptions): Promise<Router>;
|
package/dist/router.js
CHANGED
|
@@ -3,8 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createRouter = createRouter;
|
|
4
4
|
const express_1 = require("express");
|
|
5
5
|
const client_1 = require("./client");
|
|
6
|
+
/**
|
|
7
|
+
* 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
|
+
}
|
|
6
27
|
async function createRouter(options) {
|
|
7
|
-
const { config, logger } = options;
|
|
28
|
+
const { config, logger, auth } = options;
|
|
8
29
|
const baseUrl = config.getString('litellm.baseUrl');
|
|
9
30
|
const masterKey = config.getString('litellm.masterKey');
|
|
10
31
|
const client = new client_1.LiteLLMClient({ baseUrl, masterKey });
|
|
@@ -12,9 +33,13 @@ async function createRouter(options) {
|
|
|
12
33
|
router.get('/health', (_req, res) => {
|
|
13
34
|
res.json({ status: 'ok' });
|
|
14
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).
|
|
15
39
|
router.get('/user/info', async (req, res) => {
|
|
16
40
|
try {
|
|
17
|
-
const
|
|
41
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
42
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
18
43
|
const userInfo = await client.getUserInfo(userId);
|
|
19
44
|
res.json(userInfo);
|
|
20
45
|
}
|
|
@@ -25,7 +50,8 @@ async function createRouter(options) {
|
|
|
25
50
|
});
|
|
26
51
|
router.get('/keys', async (req, res) => {
|
|
27
52
|
try {
|
|
28
|
-
const
|
|
53
|
+
const tokenUserId = await resolveUserId(req, auth);
|
|
54
|
+
const userId = tokenUserId ?? req.query.user_id;
|
|
29
55
|
const keys = await client.listKeys(userId);
|
|
30
56
|
res.json(keys);
|
|
31
57
|
}
|
|
@@ -36,7 +62,12 @@ async function createRouter(options) {
|
|
|
36
62
|
});
|
|
37
63
|
router.post('/keys/generate', async (req, res) => {
|
|
38
64
|
try {
|
|
39
|
-
const
|
|
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
|
+
};
|
|
40
71
|
const result = await client.generateKey(request);
|
|
41
72
|
res.json(result);
|
|
42
73
|
}
|
|
@@ -70,6 +101,45 @@ async function createRouter(options) {
|
|
|
70
101
|
res.status(500).json({ error: error.message });
|
|
71
102
|
}
|
|
72
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
|
+
});
|
|
73
143
|
router.get('/usage', async (req, res) => {
|
|
74
144
|
try {
|
|
75
145
|
const { start_date, end_date, user_id, group_by } = req.query;
|
|
@@ -77,7 +147,9 @@ async function createRouter(options) {
|
|
|
77
147
|
res.status(400).json({ error: 'start_date and end_date are required' });
|
|
78
148
|
return;
|
|
79
149
|
}
|
|
80
|
-
const
|
|
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);
|
|
81
153
|
res.json(usage);
|
|
82
154
|
}
|
|
83
155
|
catch (error) {
|
package/dist/types.cjs.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/types.ts"],
|
|
4
|
-
"sourcesContent": ["export interface UserInfo {\n user_id: string;\n email
|
|
4
|
+
"sourcesContent": ["export interface UserInfo {\n user_id: string;\n user_email?: string;\n email?: string;\n teams?: string[];\n models?: string[];\n max_budget?: number;\n spend?: number;\n current_spend?: number;\n soft_limit?: number;\n hard_limit?: number;\n}\n\nexport interface TeamMember {\n user_id: string;\n role: 'admin' | 'user';\n}\n\nexport interface TeamInfo {\n team_id: string;\n team_alias?: string;\n max_budget?: number;\n spend: number;\n members_with_roles?: TeamMember[];\n models?: string[];\n tpm_limit?: number;\n rpm_limit?: number;\n}\n\nexport interface VirtualKey {\n key: string;\n key_alias?: string;\n created_at: string;\n expires_at?: string;\n spend: number;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n user_id?: string;\n}\n\nexport interface ModelInfo {\n model_name: string;\n mode: string;\n supports_function_calling?: boolean;\n supports_vision?: boolean;\n input_cost_per_token?: number;\n output_cost_per_token?: number;\n}\n\nexport interface UsageMetrics {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n usage_by_model: Record<string, {\n total_spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n }>;\n daily_usage: Array<{\n date: string;\n spend: number;\n total_tokens: number;\n prompt_tokens: number;\n completion_tokens: number;\n }>;\n}\n\nexport interface GenerateKeyRequest {\n alias?: string;\n models?: string[];\n duration?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n user_id?: string;\n}\n\nexport interface GenerateKeyResponse {\n key: string;\n key_alias?: string;\n expires_at?: string;\n max_budget?: number;\n tpm_limit?: number;\n rpm_limit?: number;\n models?: string[];\n}\n\nexport interface DeleteKeyRequest {\n keys: string[];\n}\n\nexport interface LiteLLMConfig {\n baseUrl: string;\n masterKey: string;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
export interface UserInfo {
|
|
2
2
|
user_id: string;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
user_email?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
teams?: string[];
|
|
6
|
+
models?: string[];
|
|
6
7
|
max_budget?: number;
|
|
8
|
+
spend?: number;
|
|
7
9
|
current_spend?: number;
|
|
8
10
|
soft_limit?: number;
|
|
9
11
|
hard_limit?: number;
|
|
10
12
|
}
|
|
13
|
+
export interface TeamMember {
|
|
14
|
+
user_id: string;
|
|
15
|
+
role: 'admin' | 'user';
|
|
16
|
+
}
|
|
17
|
+
export interface TeamInfo {
|
|
18
|
+
team_id: string;
|
|
19
|
+
team_alias?: string;
|
|
20
|
+
max_budget?: number;
|
|
21
|
+
spend: number;
|
|
22
|
+
members_with_roles?: TeamMember[];
|
|
23
|
+
models?: string[];
|
|
24
|
+
tpm_limit?: number;
|
|
25
|
+
rpm_limit?: number;
|
|
26
|
+
}
|
|
11
27
|
export interface VirtualKey {
|
|
12
28
|
key: string;
|
|
13
29
|
key_alias?: string;
|